From 6844ff5a3f01203a26514ae6ee80387bab43473a Mon Sep 17 00:00:00 2001 From: Antoine Guenet <age@odoo.com> Date: Fri, 25 Feb 2022 09:52:16 +0000 Subject: [PATCH] [FIX] web_editor: apply format, font-size and color collapsed When the selection is collapsed it was impossible to set a format (bold, italic, underline, strikethrough), a font-size or a color. This makes it possible by inserting and selecting a zero-width space first, then placing the caret to its left when we're done. task-2778416 X-original-commit: b4eb0fe69bce5ca18d8450cd7118740f1d31f780 Part-of: odoo/odoo#86930 --- .../lib/odoo-editor/src/commands/commands.js | 38 +- .../static/lib/odoo-editor/src/utils/utils.js | 17 +- .../lib/odoo-editor/test/editor-test.js | 2 + .../lib/odoo-editor/test/spec/color.test.js | 42 ++ .../lib/odoo-editor/test/spec/editor.test.js | 241 ---------- .../odoo-editor/test/spec/fontSize.test.js | 7 + .../lib/odoo-editor/test/spec/format.test.js | 427 ++++++++++++++++++ 7 files changed, 524 insertions(+), 250 deletions(-) create mode 100644 addons/web_editor/static/lib/odoo-editor/test/spec/color.test.js create mode 100644 addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js diff --git a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js index 564a62326bd2..b6e60b2f44a6 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js +++ b/addons/web_editor/static/lib/odoo-editor/src/commands/commands.js @@ -16,6 +16,7 @@ import { getNormalizedCursorPosition, getSelectedNodes, getTraversedNodes, + insertAndSelectZws, insertText, isBlock, isBold, @@ -214,6 +215,7 @@ function hasColor(element, mode) { * which the wanted style should be applied */ export function applyInlineStyle(editor, applyStyle) { + getDeepRange(editor.editable, { splitText: true, select: true }); const sel = editor.document.getSelection(); const { startContainer, startOffset, endContainer, endOffset } = sel.getRangeAt(0); const { anchorNode, anchorOffset, focusNode, focusOffset } = sel; @@ -226,7 +228,7 @@ export function applyInlineStyle(editor, applyStyle) { normalizedEndContainer, normalizedEndOffset ] = getNormalizedCursorPosition(endContainer, endOffset) - const selectedTextNodes = getTraversedNodes(editor.editable).filter(node => { + const selectedTextNodes = getSelectedNodes(editor.editable).filter(node => { const atLeastOneCharFromNodeInSelection = !( (node === normalizedEndContainer && normalizedEndOffset === 0) || (node === normalizedStartContainer && normalizedStartOffset === node.textContent.length) @@ -266,7 +268,9 @@ export function applyInlineStyle(editor, applyStyle) { } applyStyle(textNode.parentElement); } - if (selectedTextNodes.length) { + if (selectedTextNodes[0] && selectedTextNodes[0].textContent === '\u200B') { + setSelection(selectedTextNodes[0], 0); + } else if (selectedTextNodes.length) { const firstNode = selectedTextNodes[0]; const lastNode = selectedTextNodes[selectedTextNodes.length - 1]; if (direction === DIRECTIONS.RIGHT) { @@ -289,21 +293,24 @@ const styles = { }, underline: { is: isUnderline, - name: 'textDecoration', + name: 'textDecorationLine', value: 'underline', }, strikeThrough: { is: isStrikeThrough, - name: 'textDecoration', + name: 'textDecorationLine', value: 'line-through', } } export function toggleFormat(editor, format) { const selection = editor.document.getSelection(); - if (!selection.rangeCount || selection.getRangeAt(0).collapsed) return; + if (!selection.rangeCount) return; + if (selection.getRangeAt(0).collapsed) { + insertAndSelectZws(selection); + } getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true }); const style = styles[format]; - const isAlreadyFormatted = getSelectedNodes(editor.editable) + const isAlreadyFormatted = !!getSelectedNodes(editor.editable) .filter(n => n.nodeType === Node.TEXT_NODE && n.nodeValue.trim().length) .find(n => style.is(n.parentElement)); applyInlineStyle(editor, el => { @@ -430,7 +437,10 @@ export const editorCommands = { */ setFontSize: (editor, size) => { const selection = editor.document.getSelection(); - if (!selection.rangeCount || selection.getRangeAt(0).collapsed) return; + if (!selection.rangeCount) return; + if (selection.getRangeAt(0).collapsed) { + insertAndSelectZws(selection); + } applyInlineStyle(editor, element => { element.style.fontSize = size; }); @@ -545,6 +555,12 @@ export const editorCommands = { colorElement(element, color, mode); return; } + const selection = editor.document.getSelection(); + let wasCollapsed = false; + if (selection.getRangeAt(0).collapsed) { + insertAndSelectZws(selection); + wasCollapsed = true; + } const range = getDeepRange(editor.editable, { splitText: true, select: true }); if (!range) return; const restoreCursor = preserveCursor(editor.document); @@ -599,6 +615,14 @@ export const editorCommands = { } } restoreCursor(); + if (wasCollapsed) { + const newSelection = editor.document.getSelection(); + const range = new Range(); + range.setStart(newSelection.anchorNode, newSelection.anchorOffset); + range.collapse(true); + newSelection.removeAllRanges(); + newSelection.addRange(range); + } }, // Table insertTable: (editor, { rowNumber = 2, colNumber = 2 } = {}) => { diff --git a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js index 201bb3a60d95..4e3a1c88cfa9 100644 --- a/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/lib/odoo-editor/src/utils/utils.js @@ -883,7 +883,7 @@ export function isItalic(node) { * @returns {boolean} */ export function isUnderline(node) { - return getComputedStyle(closestElement(node)).textDecoration === 'underline'; + return getComputedStyle(closestElement(node)).textDecorationLine === 'underline'; } /** * Return true if the given node appears struck through. @@ -892,7 +892,7 @@ export function isUnderline(node) { * @returns {boolean} */ export function isStrikeThrough(node) { - return getComputedStyle(closestElement(node)).textDecoration === 'line-through'; + return getComputedStyle(closestElement(node)).textDecorationLine === 'line-through'; } export function isUnbreakable(node) { @@ -1415,6 +1415,7 @@ export function insertText(sel, content) { sel.getRangeAt(0).insertNode(txt); restore(); setSelection(...boundariesOut(txt), false); + return txt; } /** @@ -1469,6 +1470,18 @@ export function fillEmpty(el) { } return fillers; } +/** + * Takes a selection (assumed to be collapsed) and insert a zero-width space at + * its anchor point. Then, select that zero-width space. + * + * @param {Selection} selection + */ +export function insertAndSelectZws(selection) { + const offset = selection.anchorOffset; + const zws = insertText(selection, '\u200B'); + splitTextNode(zws, offset); + selection.getRangeAt(0).selectNode(zws); +} /** * Removes the given node if invisible and all its invisible ancestors. * diff --git a/addons/web_editor/static/lib/odoo-editor/test/editor-test.js b/addons/web_editor/static/lib/odoo-editor/test/editor-test.js index e0384918e048..48b1099af6cf 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/editor-test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/editor-test.js @@ -1,9 +1,11 @@ import './spec/utils.test.js'; import './spec/align.test.js'; +import './spec/color.test.js'; import './spec/editor.test.js'; import './spec/list.test.js'; import './spec/link.test.js'; import './spec/fontSize.test.js'; +import './spec/format.test.js'; import './spec/insertHTML.test.js'; import './spec/fontAwesome.test.js'; import './spec/autostep.test.js'; diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/color.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/color.test.js new file mode 100644 index 000000000000..78261c9450ba --- /dev/null +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/color.test.js @@ -0,0 +1,42 @@ +import { BasicEditor, testEditor } from '../utils.js'; + +const setColor = (color, mode) => { + return async editor => { + await editor.execCommand('applyColor', color, mode); + }; +}; + +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: setColor('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>', + }); + }); + it('should apply a background 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: setColor('rgb(255, 0, 0)', 'backgroundColor'), + contentAfter: '<p>a<font>b<span>c</span></font>' + + '<font style="background-color: rgb(255, 0, 0);"><span>[def]</span></font>' + + '<font><span>g</span>h</font>i</p>', + }); + }); + it('should get ready to type with a different color', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: setColor('rgb(255, 0, 0)', 'color'), + contentAfter: '<p>ab<font style="color: rgb(255, 0, 0);">[]\u200B</font>cd</p>', + }); + }); + it('should get ready to type with a different background color', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: setColor('rgb(255, 0, 0)', 'backgroundColor'), + contentAfter: '<p>ab<font style="background-color: rgb(255, 0, 0);">[]\u200B</font>cd</p>', + }); + }); +}); diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js index 759648c867ef..8222b24987bd 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/editor.test.js @@ -3016,235 +3016,6 @@ X[] }); }); - describe('applyInlineStyle', () => { - it('should apply style to selection only', async () => { - await testEditor(BasicEditor, { - contentBefore: '<p>a<span>[b<span>c]d</span>e</span>f</p>', - stepFunction: editor => applyInlineStyle(editor, (el) => el.style.color = 'tomato'), - contentAfter: '<p>a<span><span style="color: tomato;">[b</span><span><span style="color: tomato;">c]</span>d</span>e</span>f</p>', - }); - }); - }); - - describe('setTagName', () => { - describe('to paragraph', () => { - it('should turn a heading 1 into a paragraph', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>ab[]cd</h1>', - stepFunction: editor => editor.execCommand('setTag', 'p'), - contentAfter: '<p>ab[]cd</p>', - }); - }); - it('should turn a heading 1 into a paragraph (character selected)', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b]c</h1>', - stepFunction: editor => editor.execCommand('setTag', 'p'), - contentAfter: '<p>a[b]c</p>', - }); - }); - it('should turn a heading 1, a paragraph and a heading 2 into three paragraphs', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b</h1><p>cd</p><h2>e]f</h2>', - stepFunction: editor => editor.execCommand('setTag', 'p'), - contentAfter: '<p>a[b</p><p>cd</p><p>e]f</p>', - }); - }); - it.skip('should turn a heading 1 into a paragraph after a triple click', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>[ab</h1><h2>]cd</h2>', - stepFunction: editor => editor.execCommand('setTag', 'p'), - contentAfter: '<p>[ab</p><h2>]cd</h2>', - }); - }); - it('should not turn a div into a paragraph', async () => { - await testEditor(BasicEditor, { - contentBefore: '<div>[ab]</div>', - stepFunction: editor => editor.execCommand('setTag', 'p'), - contentAfter: '<div><p>[ab]</p></div>', - }); - }); - }); - describe('to heading 1', () => { - it('should turn a paragraph into a heading 1', async () => { - await testEditor(BasicEditor, { - contentBefore: '<p>ab[]cd</p>', - stepFunction: editor => editor.execCommand('setTag', 'h1'), - contentAfter: '<h1>ab[]cd</h1>', - }); - }); - it('should turn a paragraph into a heading 1 (character selected)', async () => { - await testEditor(BasicEditor, { - contentBefore: '<p>a[b]c</p>', - stepFunction: editor => editor.execCommand('setTag', 'h1'), - contentAfter: '<h1>a[b]c</h1>', - }); - }); - it('should turn a paragraph, a heading 1 and a heading 2 into three headings 1', async () => { - await testEditor(BasicEditor, { - contentBefore: '<p>a[b</p><h1>cd</h1><h2>e]f</h2>', - stepFunction: editor => editor.execCommand('setTag', 'h1'), - contentAfter: '<h1>a[b</h1><h1>cd</h1><h1>e]f</h1>', - }); - }); - it.skip('should turn a paragraph into a heading 1 after a triple click', async () => { - await testEditor(BasicEditor, { - contentBefore: '<p>[ab</p><h2>]cd</h2>', - stepFunction: editor => editor.execCommand('setTag', 'h1'), - contentAfter: '<h1>[ab</h1><h2>]cd</h2>', - }); - }); - it('should not turn a div into a heading 1', async () => { - await testEditor(BasicEditor, { - contentBefore: '<div>[ab]</div>', - stepFunction: editor => editor.execCommand('setTag', 'h1'), - contentAfter: '<div><h1>[ab]</h1></div>', - }); - }); - it('should remove the background image while turning a p>font into a heading 1>span', async () => { - await testEditor(BasicEditor, { - contentBefore: '<div><p><font class="text-gradient" style="background-image: linear-gradient(135deg, rgb(255, 204, 51) 0%, rgb(226, 51, 255) 100%);">[ab]</font></p></div>', - stepFunction: editor => editor.execCommand('setTag', 'h1'), - contentAfter: '<div><h1><span style="">[ab]</span></h1></div>', - }); - }); - }); - describe('to heading 2', () => { - it('should turn a heading 1 into a heading 2', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>ab[]cd</h1>', - stepFunction: editor => editor.execCommand('setTag', 'h2'), - contentAfter: '<h2>ab[]cd</h2>', - }); - }); - it('should turn a heading 1 into a heading 2 (character selected)', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b]c</h1>', - stepFunction: editor => editor.execCommand('setTag', 'h2'), - contentAfter: '<h2>a[b]c</h2>', - }); - }); - it('should turn a heading 1, a heading 2 and a paragraph into three headings 2', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b</h1><h2>cd</h2><p>e]f</p>', - stepFunction: editor => editor.execCommand('setTag', 'h2'), - contentAfter: '<h2>a[b</h2><h2>cd</h2><h2>e]f</h2>', - }); - }); - it.skip('should turn a paragraph into a heading 2 after a triple click', async () => { - await testEditor(BasicEditor, { - contentBefore: '<p>[ab</p><h1>]cd</h1>', - stepFunction: editor => editor.execCommand('setTag', 'h2'), - contentAfter: '<h2>[ab</h2><h1>]cd</h1>', - }); - }); - it('should not turn a div into a heading 2', async () => { - await testEditor(BasicEditor, { - contentBefore: '<div>[ab]</div>', - stepFunction: editor => editor.execCommand('setTag', 'h2'), - contentAfter: '<div><h2>[ab]</h2></div>', - }); - }); - }); - describe('to heading 3', () => { - it('should turn a heading 1 into a heading 3', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>ab[]cd</h1>', - stepFunction: editor => editor.execCommand('setTag', 'h3'), - contentAfter: '<h3>ab[]cd</h3>', - }); - }); - it('should turn a heading 1 into a heading 3 (character selected)', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b]c</h1>', - stepFunction: editor => editor.execCommand('setTag', 'h3'), - contentAfter: '<h3>a[b]c</h3>', - }); - }); - it('should turn a heading 1, a paragraph and a heading 2 into three headings 3', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b</h1><p>cd</p><h2>e]f</h2>', - stepFunction: editor => editor.execCommand('setTag', 'h3'), - contentAfter: '<h3>a[b</h3><h3>cd</h3><h3>e]f</h3>', - }); - }); - it.skip('should turn a paragraph into a heading 3 after a triple click', async () => { - await testEditor(BasicEditor, { - contentBefore: '<p>[ab</p><h1>]cd</h1>', - stepFunction: editor => editor.execCommand('setTag', 'h3'), - contentAfter: '<h3>[ab</h3><h1>]cd</h1>', - }); - }); - it('should not turn a div into a heading 3', async () => { - await testEditor(BasicEditor, { - contentBefore: '<div>[ab]</div>', - stepFunction: editor => editor.execCommand('setTag', 'h3'), - contentAfter: '<div><h3>[ab]</h3></div>', - }); - }); - }); - describe('to pre', () => { - it('should turn a heading 1 into a pre', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>ab[]cd</h1>', - stepFunction: editor => editor.execCommand('setTag', 'pre'), - contentAfter: '<pre>ab[]cd</pre>', - }); - }); - it('should turn a heading 1 into a pre (character selected)', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b]c</h1>', - stepFunction: editor => editor.execCommand('setTag', 'pre'), - contentAfter: '<pre>a[b]c</pre>', - }); - }); - it('should turn a heading 1 a pre and a paragraph into three pres', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b</h1><pre>cd</pre><p>e]f</p>', - stepFunction: editor => editor.execCommand('setTag', 'pre'), - contentAfter: '<pre>a[b</pre><pre>cd</pre><pre>e]f</pre>', - }); - }); - }); - describe('to blockquote', () => { - it('should turn a blockquote into a paragraph', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>ab[]cd</h1>', - stepFunction: editor => editor.execCommand('setTag', 'blockquote'), - contentAfter: '<blockquote>ab[]cd</blockquote>', - }); - }); - it('should turn a heading 1 into a blockquote (character selected)', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b]c</h1>', - stepFunction: editor => editor.execCommand('setTag', 'blockquote'), - contentAfter: '<blockquote>a[b]c</blockquote>', - }); - }); - it('should turn a heading 1, a paragraph and a heading 2 into three blockquote', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>a[b</h1><p>cd</p><h2>e]f</h2>', - stepFunction: editor => editor.execCommand('setTag', 'blockquote'), - contentAfter: - '<blockquote>a[b</blockquote><blockquote>cd</blockquote><blockquote>e]f</blockquote>', - }); - }); - it.skip('should turn a heading 1 into a blockquote after a triple click', async () => { - await testEditor(BasicEditor, { - contentBefore: '<h1>[ab</h1><h2>]cd</h2>', - stepFunction: editor => editor.execCommand('setTag', 'blockquote'), - contentAfter: '<blockquote>[ab</blockquote><h2>]cd</h2>', - }); - }); - it('should not turn a div into a blockquote', async () => { - await testEditor(BasicEditor, { - contentBefore: '<div>[ab]</div>', - stepFunction: editor => editor.execCommand('setTag', 'blockquote'), - contentAfter: '<div><blockquote>[ab]</blockquote></div>', - }); - }); - }); - }); - describe('getTraversedNodes', () => { it('should return the anchor node of a collapsed selection', async () => { await testEditor(BasicEditor, { @@ -3439,16 +3210,4 @@ 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>', - }); - }); - }); }); diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/fontSize.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/fontSize.test.js index 675f4860c94a..92ecb2122493 100644 --- a/addons/web_editor/static/lib/odoo-editor/test/spec/fontSize.test.js +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/fontSize.test.js @@ -22,5 +22,12 @@ describe('FontSize', () => { contentAfter: '<h1><span style="font-size: 36px;">[ab]</span></h1><p>cd</p>', }); }); + it('should get ready to type with a different font size', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: setFontSize('36px'), + contentAfter: '<p>ab<span style="font-size: 36px;">[]\u200B</span>cd</p>', + }); + }); }); }); diff --git a/addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js b/addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js new file mode 100644 index 000000000000..77e86d31c799 --- /dev/null +++ b/addons/web_editor/static/lib/odoo-editor/test/spec/format.test.js @@ -0,0 +1,427 @@ +import { BasicEditor, testEditor } from '../utils.js'; +import { applyInlineStyle } from '../../src/commands/commands.js'; + +const bold = async editor => { + await editor.execCommand('bold'); +}; +const italic = async editor => { + await editor.execCommand('italic'); +}; +const underline = async editor => { + await editor.execCommand('underline'); +}; +const strikeThrough = async editor => { + await editor.execCommand('strikeThrough'); +}; + +describe('Format', () => { + describe('bold', () => { + it('should make a few characters bold', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[cde]fg</p>', + stepFunction: bold, + contentAfter: '<p>ab<span style="font-weight: bolder;">[cde]</span>fg</p>', + }); + }); + it('should make a few characters not bold', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="font-weight: bolder;">ab[cde]fg</span></p>', + stepFunction: bold, + contentAfter: '<p><span style="font-weight: bolder;">ab<span style="font-weight: 400;">[cde]</span>fg</span></p>', + }); + }); + it('should make a whole heading bold after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1><span style="font-weight: normal;">[ab</span></h1><p>]cd</p>', + stepFunction: bold, + // TODO: ideally should restore regular h1 without span instead. + contentAfter: '<h1><span style="font-weight: bolder;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should make a whole heading not bold after a triple click (heading is considered bold)', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>[ab</h1><p>]cd</p>', + stepFunction: bold, + contentAfter: '<h1><span style="font-weight: normal;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should get ready to type in bold', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: bold, + contentAfter: '<p>ab<span style="font-weight: bolder;">[]\u200B</span>cd</p>', + }); + }); + it('should get ready to type in not bold', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="font-weight: bolder;">ab[]cd</span></p>', + stepFunction: bold, + contentAfter: '<p><span style="font-weight: bolder;">ab<span style="font-weight: 400;">[]\u200B</span>cd</span></p>', + }); + }); + }); + describe('italic', () => { + it('should make a few characters italic', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[cde]fg</p>', + stepFunction: italic, + contentAfter: '<p>ab<span style="font-style: italic;">[cde]</span>fg</p>', + }); + }); + it('should make a few characters not italic', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="font-style: italic;">ab[cde]fg</span></p>', + stepFunction: italic, + contentAfter: '<p><span style="font-style: italic;">ab<span style="font-style: normal;">[cde]</span>fg</span></p>', + }); + }); + it('should make a whole heading italic after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>[ab</h1><p>]cd</p>', + stepFunction: italic, + contentAfter: '<h1><span style="font-style: italic;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should make a whole heading not italic after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1><span style="font-style: italic;">[ab</span></h1><p>]cd</p>', + stepFunction: italic, + // TODO: ideally should restore regular h1 without span instead. + contentAfter: '<h1><span style="font-style: normal;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should get ready to type in italic', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: italic, + contentAfter: '<p>ab<span style="font-style: italic;">[]\u200B</span>cd</p>', + }); + }); + it('should get ready to type in not italic', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="font-style: italic;">ab[]cd</span></p>', + stepFunction: italic, + contentAfter: '<p><span style="font-style: italic;">ab<span style="font-style: normal;">[]\u200B</span>cd</span></p>', + }); + }); + }); + describe('underline', () => { + it('should make a few characters underline', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[cde]fg</p>', + stepFunction: underline, + contentAfter: '<p>ab<span style="text-decoration-line: underline;">[cde]</span>fg</p>', + }); + }); + it('should make a few characters not underline', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="text-decoration-line: underline;">ab[cde]fg</span></p>', + stepFunction: underline, + contentAfter: '<p><span style="text-decoration-line: underline;">ab<span style="text-decoration-line: none;">[cde]</span>fg</span></p>', + }); + }); + it('should make a whole heading underline after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>[ab</h1><p>]cd</p>', + stepFunction: underline, + contentAfter: '<h1><span style="text-decoration-line: underline;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should make a whole heading not underline after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1><span style="text-decoration-line: underline;">[ab</span></h1><p>]cd</p>', + stepFunction: underline, + // TODO: ideally should restore regular h1 without span instead. + contentAfter: '<h1><span style="text-decoration-line: none;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should get ready to type in underline', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: underline, + contentAfter: '<p>ab<span style="text-decoration-line: underline;">[]\u200B</span>cd</p>', + }); + }); + it('should get ready to type in not underline', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="text-decoration-line: underline;">ab[]cd</span></p>', + stepFunction: underline, + contentAfter: '<p><span style="text-decoration-line: underline;">ab<span style="text-decoration-line: none;">[]\u200B</span>cd</span></p>', + }); + }); + }); + describe('strikeThrough', () => { + it('should make a few characters strikeThrough', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[cde]fg</p>', + stepFunction: strikeThrough, + contentAfter: '<p>ab<span style="text-decoration-line: line-through;">[cde]</span>fg</p>', + }); + }); + it('should make a few characters not strikeThrough', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="text-decoration-line: line-through;">ab[cde]fg</span></p>', + stepFunction: strikeThrough, + contentAfter: '<p><span style="text-decoration-line: line-through;">ab<span style="text-decoration-line: none;">[cde]</span>fg</span></p>', + }); + }); + it('should make a whole heading strikeThrough after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>[ab</h1><p>]cd</p>', + stepFunction: strikeThrough, + contentAfter: '<h1><span style="text-decoration-line: line-through;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should make a whole heading not strikeThrough after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1><span style="text-decoration-line: line-through;">[ab</span></h1><p>]cd</p>', + stepFunction: strikeThrough, + // TODO: ideally should restore regular h1 without span instead. + contentAfter: '<h1><span style="text-decoration-line: none;">[ab]</span></h1><p>cd</p>', + }); + }); + it('should get ready to type in strikeThrough', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: strikeThrough, + contentAfter: '<p>ab<span style="text-decoration-line: line-through;">[]\u200B</span>cd</p>', + }); + }); + it('should get ready to type in not strikeThrough', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p><span style="text-decoration-line: line-through;">ab[]cd</span></p>', + stepFunction: strikeThrough, + contentAfter: '<p><span style="text-decoration-line: line-through;">ab<span style="text-decoration-line: none;">[]\u200B</span>cd</span></p>', + }); + }); + }); +}); + +describe('applyInlineStyle', () => { + it('should apply style to selection only', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>a<span>[b<span>c]d</span>e</span>f</p>', + stepFunction: editor => applyInlineStyle(editor, (el) => el.style.color = 'tomato'), + contentAfter: '<p>a<span><span style="color: tomato;">[b</span><span><span style="color: tomato;">c]</span>d</span>e</span>f</p>', + }); + }); +}); + +describe('setTagName', () => { + describe('to paragraph', () => { + it('should turn a heading 1 into a paragraph', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>ab[]cd</h1>', + stepFunction: editor => editor.execCommand('setTag', 'p'), + contentAfter: '<p>ab[]cd</p>', + }); + }); + it('should turn a heading 1 into a paragraph (character selected)', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b]c</h1>', + stepFunction: editor => editor.execCommand('setTag', 'p'), + contentAfter: '<p>a[b]c</p>', + }); + }); + it('should turn a heading 1, a paragraph and a heading 2 into three paragraphs', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b</h1><p>cd</p><h2>e]f</h2>', + stepFunction: editor => editor.execCommand('setTag', 'p'), + contentAfter: '<p>a[b</p><p>cd</p><p>e]f</p>', + }); + }); + it.skip('should turn a heading 1 into a paragraph after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>[ab</h1><h2>]cd</h2>', + stepFunction: editor => editor.execCommand('setTag', 'p'), + contentAfter: '<p>[ab</p><h2>]cd</h2>', + }); + }); + it('should not turn a div into a paragraph', async () => { + await testEditor(BasicEditor, { + contentBefore: '<div>[ab]</div>', + stepFunction: editor => editor.execCommand('setTag', 'p'), + contentAfter: '<div><p>[ab]</p></div>', + }); + }); + }); + describe('to heading 1', () => { + it('should turn a paragraph into a heading 1', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>ab[]cd</p>', + stepFunction: editor => editor.execCommand('setTag', 'h1'), + contentAfter: '<h1>ab[]cd</h1>', + }); + }); + it('should turn a paragraph into a heading 1 (character selected)', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>a[b]c</p>', + stepFunction: editor => editor.execCommand('setTag', 'h1'), + contentAfter: '<h1>a[b]c</h1>', + }); + }); + it('should turn a paragraph, a heading 1 and a heading 2 into three headings 1', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>a[b</p><h1>cd</h1><h2>e]f</h2>', + stepFunction: editor => editor.execCommand('setTag', 'h1'), + contentAfter: '<h1>a[b</h1><h1>cd</h1><h1>e]f</h1>', + }); + }); + it.skip('should turn a paragraph into a heading 1 after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>[ab</p><h2>]cd</h2>', + stepFunction: editor => editor.execCommand('setTag', 'h1'), + contentAfter: '<h1>[ab</h1><h2>]cd</h2>', + }); + }); + it('should not turn a div into a heading 1', async () => { + await testEditor(BasicEditor, { + contentBefore: '<div>[ab]</div>', + stepFunction: editor => editor.execCommand('setTag', 'h1'), + contentAfter: '<div><h1>[ab]</h1></div>', + }); + }); + it('should remove the background image while turning a p>font into a heading 1>span', async () => { + await testEditor(BasicEditor, { + contentBefore: '<div><p><font class="text-gradient" style="background-image: linear-gradient(135deg, rgb(255, 204, 51) 0%, rgb(226, 51, 255) 100%);">[ab]</font></p></div>', + stepFunction: editor => editor.execCommand('setTag', 'h1'), + contentAfter: '<div><h1><span style="">[ab]</span></h1></div>', + }); + }); + }); + describe('to heading 2', () => { + it('should turn a heading 1 into a heading 2', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>ab[]cd</h1>', + stepFunction: editor => editor.execCommand('setTag', 'h2'), + contentAfter: '<h2>ab[]cd</h2>', + }); + }); + it('should turn a heading 1 into a heading 2 (character selected)', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b]c</h1>', + stepFunction: editor => editor.execCommand('setTag', 'h2'), + contentAfter: '<h2>a[b]c</h2>', + }); + }); + it('should turn a heading 1, a heading 2 and a paragraph into three headings 2', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b</h1><h2>cd</h2><p>e]f</p>', + stepFunction: editor => editor.execCommand('setTag', 'h2'), + contentAfter: '<h2>a[b</h2><h2>cd</h2><h2>e]f</h2>', + }); + }); + it.skip('should turn a paragraph into a heading 2 after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>[ab</p><h1>]cd</h1>', + stepFunction: editor => editor.execCommand('setTag', 'h2'), + contentAfter: '<h2>[ab</h2><h1>]cd</h1>', + }); + }); + it('should not turn a div into a heading 2', async () => { + await testEditor(BasicEditor, { + contentBefore: '<div>[ab]</div>', + stepFunction: editor => editor.execCommand('setTag', 'h2'), + contentAfter: '<div><h2>[ab]</h2></div>', + }); + }); + }); + describe('to heading 3', () => { + it('should turn a heading 1 into a heading 3', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>ab[]cd</h1>', + stepFunction: editor => editor.execCommand('setTag', 'h3'), + contentAfter: '<h3>ab[]cd</h3>', + }); + }); + it('should turn a heading 1 into a heading 3 (character selected)', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b]c</h1>', + stepFunction: editor => editor.execCommand('setTag', 'h3'), + contentAfter: '<h3>a[b]c</h3>', + }); + }); + it('should turn a heading 1, a paragraph and a heading 2 into three headings 3', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b</h1><p>cd</p><h2>e]f</h2>', + stepFunction: editor => editor.execCommand('setTag', 'h3'), + contentAfter: '<h3>a[b</h3><h3>cd</h3><h3>e]f</h3>', + }); + }); + it.skip('should turn a paragraph into a heading 3 after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<p>[ab</p><h1>]cd</h1>', + stepFunction: editor => editor.execCommand('setTag', 'h3'), + contentAfter: '<h3>[ab</h3><h1>]cd</h1>', + }); + }); + it('should not turn a div into a heading 3', async () => { + await testEditor(BasicEditor, { + contentBefore: '<div>[ab]</div>', + stepFunction: editor => editor.execCommand('setTag', 'h3'), + contentAfter: '<div><h3>[ab]</h3></div>', + }); + }); + }); + describe('to pre', () => { + it('should turn a heading 1 into a pre', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>ab[]cd</h1>', + stepFunction: editor => editor.execCommand('setTag', 'pre'), + contentAfter: '<pre>ab[]cd</pre>', + }); + }); + it('should turn a heading 1 into a pre (character selected)', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b]c</h1>', + stepFunction: editor => editor.execCommand('setTag', 'pre'), + contentAfter: '<pre>a[b]c</pre>', + }); + }); + it('should turn a heading 1 a pre and a paragraph into three pres', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b</h1><pre>cd</pre><p>e]f</p>', + stepFunction: editor => editor.execCommand('setTag', 'pre'), + contentAfter: '<pre>a[b</pre><pre>cd</pre><pre>e]f</pre>', + }); + }); + }); + describe('to blockquote', () => { + it('should turn a blockquote into a paragraph', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>ab[]cd</h1>', + stepFunction: editor => editor.execCommand('setTag', 'blockquote'), + contentAfter: '<blockquote>ab[]cd</blockquote>', + }); + }); + it('should turn a heading 1 into a blockquote (character selected)', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b]c</h1>', + stepFunction: editor => editor.execCommand('setTag', 'blockquote'), + contentAfter: '<blockquote>a[b]c</blockquote>', + }); + }); + it('should turn a heading 1, a paragraph and a heading 2 into three blockquote', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>a[b</h1><p>cd</p><h2>e]f</h2>', + stepFunction: editor => editor.execCommand('setTag', 'blockquote'), + contentAfter: + '<blockquote>a[b</blockquote><blockquote>cd</blockquote><blockquote>e]f</blockquote>', + }); + }); + it.skip('should turn a heading 1 into a blockquote after a triple click', async () => { + await testEditor(BasicEditor, { + contentBefore: '<h1>[ab</h1><h2>]cd</h2>', + stepFunction: editor => editor.execCommand('setTag', 'blockquote'), + contentAfter: '<blockquote>[ab</blockquote><h2>]cd</h2>', + }); + }); + it('should not turn a div into a blockquote', async () => { + await testEditor(BasicEditor, { + contentBefore: '<div>[ab]</div>', + stepFunction: editor => editor.execCommand('setTag', 'blockquote'), + contentAfter: '<div><blockquote>[ab]</blockquote></div>', + }); + }); + }); +}); -- GitLab