From 7c19e7389e651025baf6ba6c79f0a9cd44debb97 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 16 Sep 2022 10:36:11 +0000 Subject: [PATCH] [ADD] web_editor: multiline tab and shift+tab This adds the possibility to remove leading tabulations with shift+tab, as well as supporting tabulation and untabulation over multiple blocks. task-2792099 closes odoo/odoo#100386 Signed-off-by: David Monjoie (dmo) --- .../js/editor/odoo-editor/src/OdooEditor.js | 88 +++- .../editor/odoo-editor/test/spec/tabs.test.js | 412 ++++++++++++++++-- 2 files changed, 445 insertions(+), 55 deletions(-) diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js index 4dc53088887..5a5f07a21ec 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js @@ -66,7 +66,8 @@ import { hasTableSelection, pxToFloat, parseHTML, - splitTextNode + splitTextNode, + isEditorTab, } from './utils/utils.js'; import { editorCommands } from './commands/commands.js'; import { Powerbox } from './powerbox/Powerbox.js'; @@ -3350,15 +3351,83 @@ export class OdooEditor extends EventTarget { } } else if (ev.key === 'Tab') { // Tab + const tabHtml = '\u0009\u200B'; const sel = this.document.getSelection(); - const closestTag = (closestElement(sel.anchorNode, 'li, table') || {}).tagName; - - if (closestTag === 'LI') { - this._applyCommand('indentList', ev.shiftKey ? 'outdent' : 'indent'); - } else if (closestTag === 'TABLE') { + if (closestElement(sel.anchorNode, 'table')) { this._onTabulationInTable(ev); - } else if (!ev.shiftKey) { - this.execCommand('insert', parseHTML('\u0009\u200B')); + } else if (!ev.shiftKey && sel.isCollapsed && !closestElement(sel.anchorNode, 'li')) { + // Indent text (collapsed selection). + this.execCommand('insert', parseHTML(tabHtml)); + } else { + // Indent/outdent selection. + // Split traversed nodes into list items and the rest. + const listItems = new Set(); + const nonListItems = new Set(); + for (const node of getTraversedNodes(this.editable)) { + const closestLi = closestElement(node, 'li'); + const target = closestLi || node; + if (!(target.querySelector && target.querySelector('li'))) { + if (closestLi) { + listItems.add(closestLi); + } else { + nonListItems.add(node); + } + } + } + + const restore = preserveCursor(this.document); + + // Indent/outdent list items. + for (const listItem of listItems) { + if (ev.shiftKey) { + listItem.oShiftTab(0); + } else { + listItem.oTab(0); + } + } + + // Indent/outdent the rest. + if (ev.shiftKey) { + const editorTabs = new Set( + [...nonListItems].map(node => { + const block = closestBlock(node); + if (block && isEditorTab(block.firstElementChild)) { + return block.firstElementChild; + } + }).filter(node => ( + // Filter out tabs preceded by visible text. + node && !getAdjacentPreviousSiblings(node).some(sibling => ( + sibling.nodeType === Node.TEXT_NODE && !/^[\u200B\s]*$/.test(sibling.textContent) + )) + ))); + for (const tab of editorTabs) { + let { anchorNode, anchorOffset, focusNode, focusOffset } = sel; + const updateAnchor = anchorNode === tab.nextSibling; + const updateFocus = focusNode === tab.nextSibling; + let zwsRemoved = 0; + while (tab.nextSibling && tab.nextSibling.nodeType === Node.TEXT_NODE && tab.nextSibling.textContent.startsWith('\u200B')) { + splitTextNode(tab.nextSibling, 1, DIRECTIONS.LEFT); + tab.nextSibling.remove(); + zwsRemoved++; + } + if (updateAnchor || updateFocus) { + setSelection( + updateAnchor ? tab.nextSibling : anchorNode, + updateAnchor ? Math.max(0, anchorOffset - zwsRemoved) : anchorOffset, + updateFocus ? tab.nextSibling : focusNode, + updateFocus ? Math.max(0, focusOffset - zwsRemoved) : focusOffset + ); + } + tab.remove(); + }; + } else { + const tab = parseHTML(tabHtml); + for (const block of new Set([...nonListItems].map(node => closestBlock(node)).filter(node => node))) { + block.prepend(tab.cloneNode(true)); + } + restore(); + } + this.historyStep(); } ev.preventDefault(); ev.stopPropagation(); @@ -3577,8 +3646,7 @@ export class OdooEditor extends EventTarget { } /** - * initialise the provided element to be ready for edition - * + * Initialize the provided element to be ready for edition. */ initElementForEdition(element = this.editable) { // Detect if the editable base element contain orphan inline nodes. If diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js index 93a2787d340..7210b6f25e8 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/tabs.test.js @@ -7,43 +7,48 @@ import { testEditor, } from '../utils.js'; +const TAB_WIDTH = 40; + describe('Tabs', () => { - const oeTab = (size = '40px', contenteditable = true) => { - return `\u0009\u200B` - }; - describe('insert Tabulation', () => { + const oeTab = (size, contenteditable = true) => ( + `\u0009\u200B` + ); + describe('insert tabulation', () => { it('should insert a tab character', async () => { await testEditor(BasicEditor, { contentBefore: `

a[]b

`, stepFunction: async editor => { await triggerEvent(editor.editable, 'keydown', { key: 'Tab'}); }, - contentAfterEdit: `

a${oeTab('32.8906px', false)}[]b

`, - contentAfter: `

a${oeTab('32.8906px')}[]b

`, + contentAfterEdit: `

a${oeTab(32.8906, false)}[]b

`, + contentAfter: `

a${oeTab(32.8906)}[]b

`, }); }); - it('should clear selection and insert a tab character', async () => { + it('should keep selection and insert a tab character at the beginning of the paragraph', async () => { await testEditor(BasicEditor, { contentBefore: `

a[xxx]b

`, stepFunction: async editor => { await triggerEvent(editor.editable, 'keydown', { key: 'Tab'}); }, - contentAfterEdit: `

a${oeTab('32.8906px', false)}[]b

`, - contentAfter: `

a${oeTab('32.8906px')}[]b

`, + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}a[xxx]b

`, + contentAfter: `

${oeTab(TAB_WIDTH)}a[xxx]b

`, }); }); - it('should insert two tab character', async () => { + it('should insert two tab characters', async () => { await testEditor(BasicEditor, { contentBefore: `

a[]b

`, stepFunction: async editor => { await triggerEvent(editor.editable, 'keydown', { key: 'Tab'}); await triggerEvent(editor.editable, 'keydown', { key: 'Tab'}); }, - contentAfterEdit: `

a${oeTab('32.8906px', false)}${oeTab('40px', false)}[]b

`, - contentAfter: `

a${oeTab('32.8906px')}${oeTab()}[]b

`, + contentAfterEdit: `

a${oeTab(32.8906, false)}${oeTab(TAB_WIDTH, false)}[]b

`, + contentAfter: `

a${oeTab(32.8906)}${oeTab(TAB_WIDTH)}[]b

`, }); }); - it('should insert two tab character with one char between', async () => { + it('should insert two tab characters with one char between them', async () => { await testEditor(BasicEditor, { contentBefore: `

a[]b

`, stepFunction: async editor => { @@ -51,31 +56,187 @@ describe('Tabs', () => { await insertText(editor,'a'); await triggerEvent(editor.editable, 'keydown', { key: 'Tab'}); }, - contentAfterEdit: `

a${oeTab('32.8906px', false)}a${oeTab('32.8906px', false)}[]b

`, - contentAfter: `

a${oeTab('32.8906px')}a${oeTab('32.8906px')}[]b

`, + contentAfterEdit: `

a${oeTab(32.8906, false)}a${oeTab(32.8906, false)}[]b

`, + contentAfter: `

a${oeTab(32.8906)}a${oeTab(32.8906)}[]b

`, + }); + }); + it('should insert tab characters at the beginning of two separate paragraphs', async () => { + await testEditor(BasicEditor, { + contentBefore: `

a[b

` + + `

c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}c]d

`, + contentAfter: `

${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}c]d

`, + }); + }); + it('should insert tab characters at the beginning of two separate indented paragraphs', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[b

` + + `

${oeTab()}c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}c]d

`, + contentAfter: `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}c]d

`, + }); + }); + it('should insert tab characters at the beginning of two separate paragraphs (one indented, the other not)', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[b

` + + `

c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}c]d

`, + contentAfter: `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}c]d

`, + }); + await testEditor(BasicEditor, { + contentBefore: `

a[b

` + + `

${oeTab()}c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}c]d

`, + contentAfter: `

${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}c]d

`, + }); + }); + it('should insert tab characters at the beginning of two separate paragraphs with tabs in them', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[${oeTab()}b${oeTab()}

` + + `

c${oeTab()}]d${oeTab()}

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}a[${oeTab(32.8906, false)}b${oeTab(32, false)}

` + + `

${oeTab(TAB_WIDTH, false)}c${oeTab(32.8906, false)}]d${oeTab(32, false)}

`, + contentAfter: `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}a[${oeTab(32.8906)}b${oeTab(32)}

` + + `

${oeTab(TAB_WIDTH)}c${oeTab(32.8906)}]d${oeTab(32)}

`, + }); + }); + it('should insert tab characters at the beginning of three separate blocks', async () => { + await testEditor(BasicEditor, { + contentBefore: `

xxx

` + + `

a[b

` + + `

cd

` + + `
e]f
` + + `

zzz

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

xxx

` + + `

${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}cd

` + + `
${oeTab(TAB_WIDTH, false)}e]f
` + + `

zzz

`, + contentAfter: `

xxx

` + + `

${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}cd

` + + `
${oeTab(TAB_WIDTH)}e]f
` + + `

zzz

`, + }); + }); + it('should insert tab characters at the beginning of three separate indented blocks', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}xxx

` + + `

${oeTab()}a[b

` + + `

${oeTab()}cd

` + + `
${oeTab()}e]f
` + + `

${oeTab()}zzz

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}xxx

` + + `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}cd

` + + `
${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}e]f
` + + `

${oeTab(TAB_WIDTH, false)}zzz

`, + contentAfter: `

${oeTab(TAB_WIDTH)}xxx

` + + `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}cd

` + + `
${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}e]f
` + + `

${oeTab(TAB_WIDTH)}zzz

`, + }); + }); + it('should insert tab characters at the beginning of three separate blocks of mixed indentation', async () => { + await testEditor(BasicEditor, { + contentBefore: `

xxx

` + + `

${oeTab()}${oeTab()}a[b

` + + `

${oeTab()}cd

` + + `
e]f
` + + `

zzz

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

xxx

` + + `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}cd

` + + `
${oeTab(TAB_WIDTH, false)}e]f
` + + `

zzz

`, + contentAfter: `

xxx

` + + `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}cd

` + + `
${oeTab(TAB_WIDTH)}e]f
` + + `

zzz

`, + }); + }); + it('should insert tab characters at the beginning of three separate blocks with tabs in them', async () => { + await testEditor(BasicEditor, { + contentBefore: `

xxx

` + + `

${oeTab()}a[${oeTab()}b${oeTab()}

` + + `

c${oeTab()}d${oeTab()}

` + + `
e${oeTab()}]f
` + + `

zzz

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

xxx

` + + `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}a[${oeTab(32.8906, false)}b${oeTab(32, false)}

` + + `

${oeTab(TAB_WIDTH, false)}c${oeTab(25.7969, false)}d${oeTab(22.2031, false)}

` + + `
${oeTab(TAB_WIDTH, false)}e${oeTab(32.8906, false)}]f
` + + `

zzz

`, + contentAfter: `

xxx

` + + `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}a[${oeTab(32.8906)}b${oeTab(32)}

` + + `

${oeTab(TAB_WIDTH)}c${oeTab(25.7969)}d${oeTab(22.2031)}

` + + `
${oeTab(TAB_WIDTH)}e${oeTab(32.8906)}]f
` + + `

zzz

`, + }); + }); + it('should insert tab characters in blocks and indent lists', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[${oeTab()}b${oeTab()}

` + + `` + + `
f${oeTab()}]g
`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}${oeTab(TAB_WIDTH, false)}a[${oeTab(32.8906, false)}b${oeTab(32, false)}

` + + `` + + `
${oeTab(TAB_WIDTH, false)}f${oeTab(34.6719, false)}]g
`, + contentAfter: `

${oeTab(TAB_WIDTH)}${oeTab(TAB_WIDTH)}a[${oeTab(32.8906)}b${oeTab(32)}

` + + `` + + `
${oeTab(TAB_WIDTH)}f${oeTab(34.6719)}]g
`, }); }); }); - describe('delete backward Tabulation', () => { + describe('delete backward tabulation', () => { it('should remove one tab character', async () => { await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}[]b

`, + contentBefore: `

a${oeTab(32.8906)}[]b

`, stepFunction: async editor => { await deleteBackward(editor); }, contentAfter: `

a[]b

`, }); await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}[]${oeTab()}b

`, + contentBefore: `

a${oeTab(32.8906)}[]${oeTab()}b

`, stepFunction: async editor => { await deleteBackward(editor); }, - contentAfter: `

a[]${oeTab('32.8906px')}b

`, + contentAfter: `

a[]${oeTab(32.8906)}b

`, }); }); - it('should remove two tab character', async () => { + it('should remove two tab characters', async () => { await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}${oeTab()}[]b

`, + contentBefore: `

a${oeTab(32.8906)}${oeTab()}[]b

`, stepFunction: async editor => { await deleteBackward(editor); await deleteBackward(editor); @@ -83,17 +244,17 @@ describe('Tabs', () => { contentAfter: `

a[]b

`, }); await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}${oeTab()}[]${oeTab()}b

`, + contentBefore: `

a${oeTab(32.8906)}${oeTab()}[]${oeTab()}b

`, stepFunction: async editor => { await deleteBackward(editor); await deleteBackward(editor); }, - contentAfter: `

a[]${oeTab('32.8906px')}b

`, + contentAfter: `

a[]${oeTab(32.8906)}b

`, }); }); - it('should remove three tab character', async () => { + it('should remove three tab characters', async () => { await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}${oeTab()}${oeTab()}[]b

`, + contentBefore: `

a${oeTab(32.8906)}${oeTab()}${oeTab()}[]b

`, stepFunction: async editor => { await deleteBackward(editor); await deleteBackward(editor); @@ -103,33 +264,33 @@ describe('Tabs', () => { }); }); }); - describe('delete forward Tabulation', () => { + describe('delete forward tabulation', () => { it('should remove one tab character', async () => { await testEditor(BasicEditor, { - contentBefore: `

a[]${oeTab('32.8906px')}b1

`, + contentBefore: `

a[]${oeTab(32.8906)}b1

`, stepFunction: async editor => { await deleteForward(editor); }, contentAfter: `

a[]b1

`, }); await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}[]${oeTab()}b2

`, + contentBefore: `

a${oeTab(32.8906)}[]${oeTab()}b2

`, stepFunction: async editor => { await deleteForward(editor); }, - contentAfter: `

a${oeTab('32.8906px')}[]b2

`, + contentAfter: `

a${oeTab(32.8906)}[]b2

`, }); await testEditor(BasicEditor, { - contentBefore: `

a[]${oeTab('32.8906px')}${oeTab()}b3

`, + contentBefore: `

a[]${oeTab(32.8906)}${oeTab()}b3

`, stepFunction: async editor => { await deleteForward(editor); }, - contentAfter: `

a[]${oeTab('32.8906px')}b3

`, + contentAfter: `

a[]${oeTab(32.8906)}b3

`, }); }); - it('should remove two tab character', async () => { + it('should remove two tab characters', async () => { await testEditor(BasicEditor, { - contentBefore: `

a[]${oeTab('32.8906px')}${oeTab()}b1

`, + contentBefore: `

a[]${oeTab(32.8906)}${oeTab()}b1

`, stepFunction: async editor => { await deleteForward(editor); await deleteForward(editor); @@ -137,25 +298,25 @@ describe('Tabs', () => { contentAfter: `

a[]b1

`, }); await testEditor(BasicEditor, { - contentBefore: `

a[]${oeTab('32.8906px')}${oeTab()}${oeTab()}b2

`, + contentBefore: `

a[]${oeTab(32.8906)}${oeTab()}${oeTab()}b2

`, stepFunction: async editor => { await deleteForward(editor); await deleteForward(editor); }, - contentAfter: `

a[]${oeTab('32.8906px')}b2

`, + contentAfter: `

a[]${oeTab(32.8906)}b2

`, }); await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}[]${oeTab()}${oeTab()}b3

`, + contentBefore: `

a${oeTab(32.8906)}[]${oeTab()}${oeTab()}b3

`, stepFunction: async editor => { await deleteForward(editor); await deleteForward(editor); }, - contentAfter: `

a${oeTab('32.8906px')}[]b3

`, + contentAfter: `

a${oeTab(32.8906)}[]b3

`, }); }); - it('should remove three tab character', async () => { + it('should remove three tab characters', async () => { await testEditor(BasicEditor, { - contentBefore: `

a[]${oeTab('32.8906px')}${oeTab()}${oeTab()}b

`, + contentBefore: `

a[]${oeTab(32.8906)}${oeTab()}${oeTab()}b

`, stepFunction: async editor => { await deleteForward(editor); await deleteForward(editor); @@ -165,10 +326,10 @@ describe('Tabs', () => { }); }); }); - describe('delete mixxed Tabulation', () => { - it('should remove all tab character', async () => { + describe('delete mixed tabulation', () => { + it('should remove all tab characters', async () => { await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}[]${oeTab()}b1

`, + contentBefore: `

a${oeTab(32.8906)}[]${oeTab()}b1

`, stepFunction: async editor => { await deleteForward(editor); await deleteBackward(editor); @@ -176,7 +337,7 @@ describe('Tabs', () => { contentAfter: `

a[]b1

`, }); await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}[]${oeTab()}b2

`, + contentBefore: `

a${oeTab(32.8906)}[]${oeTab()}b2

`, stepFunction: async editor => { await deleteBackward(editor); await deleteForward(editor); @@ -184,7 +345,7 @@ describe('Tabs', () => { contentAfter: `

a[]b2

`, }); await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}${oeTab()}[]${oeTab()}b3

`, + contentBefore: `

a${oeTab(32.8906)}${oeTab()}[]${oeTab()}b3

`, stepFunction: async editor => { await deleteBackward(editor); await deleteForward(editor); @@ -193,7 +354,7 @@ describe('Tabs', () => { contentAfter: `

a[]b3

`, }); await testEditor(BasicEditor, { - contentBefore: `

a${oeTab('32.8906px')}[]${oeTab()}${oeTab()}b4

`, + contentBefore: `

a${oeTab(32.8906)}[]${oeTab()}${oeTab()}b4

`, stepFunction: async editor => { await deleteForward(editor); await deleteBackward(editor); @@ -203,4 +364,165 @@ describe('Tabs', () => { }); }); }); + describe('remove tabulation with shift+tab', () => { + it('should not remove a non-leading tab character', async () => { + await testEditor(BasicEditor, { + contentBefore: `

a${oeTab()}[]b

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfterEdit: `

a${oeTab(32.8906, false)}[]b

`, + contentAfter: `

a${oeTab(32.8906)}[]b

`, + }); + }); + it('should remove a tab character', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[]b

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfter: `

a[]b

`, + }); + }); + it('should keep selection and remove a tab character from the beginning of the paragraph', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[xxx]b

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfter: `

a[xxx]b

`, + }); + }); + it('should remove two tab characters', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}${oeTab()}a[]b

`, + stepFunction: async editor => { + await triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }); + await triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }); + }, + contentAfter: `

a[]b

`, + }); + }); + it('should remove tab characters from the beginning of two separate paragraphs', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[b

` + + `

${oeTab()}c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfter: `

a[b

` + + `

c]d

`, + }); + }); + it('should remove tab characters from the beginning of two separate double-indented paragraphs', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}${oeTab()}a[b

` + + `

${oeTab()}${oeTab()}c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}a[b

` + + `

${oeTab(TAB_WIDTH, false)}c]d

`, + contentAfter: `

${oeTab(TAB_WIDTH)}a[b

` + + `

${oeTab(TAB_WIDTH)}c]d

`, + }); + }); + it('should remove tab characters from the beginning of two separate paragraphs of mixed indentations', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}${oeTab()}a[b

` + + `

${oeTab()}c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}a[b

` + + `

c]d

`, + contentAfter: `

${oeTab(TAB_WIDTH)}a[b

` + + `

c]d

`, + }); + await testEditor(BasicEditor, { + contentBefore: `

a[b

` + + `

${oeTab()}c]d

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfter: `

a[b

` + + `

c]d

`, + }); + }); + it('should remove tab characters from the beginning of two separate paragraphs with tabs in them', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}a[${oeTab()}b${oeTab()}

` + + `

c${oeTab()}]d${oeTab()}

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfterEdit: `

a[${oeTab(32.8906, false)}b${oeTab(32, false)}

` + + `

c${oeTab(32.8906, false)}]d${oeTab(32, false)}

`, + contentAfter: `

a[${oeTab(32.8906)}b${oeTab(32)}

` + + `

c${oeTab(32.8906)}]d${oeTab(32)}

`, + }); + }); + it('should remove tab characters from the beginning of three separate blocks', async () => { + await testEditor(BasicEditor, { + contentBefore: `

xxx

` + + `

${oeTab()}a[b

` + + `

${oeTab()}cd

` + + `
${oeTab()}e]f
` + + `

zzz

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfter: `

xxx

` + + `

a[b

` + + `

cd

` + + `
e]f
` + + `

zzz

`, + }); + }); + it('should remove tab characters from the beginning of three separate blocks of mixed indentation', async () => { + await testEditor(BasicEditor, { + contentBefore: `

xxx

` + + `

${oeTab()}${oeTab()}a[b

` + + `

${oeTab()}cd

` + + `
e]f
` + + `

zzz

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfterEdit: `

xxx

` + + `

${oeTab(TAB_WIDTH, false)}a[b

` + + `

cd

` + + `
e]f
` + + `

zzz

`, + contentAfter: `

xxx

` + + `

${oeTab(TAB_WIDTH)}a[b

` + + `

cd

` + + `
e]f
` + + `

zzz

`, + }); + }); + it('should remove tab characters from the beginning of three separate blocks with tabs in them', async () => { + await testEditor(BasicEditor, { + contentBefore: `

xxx

` + + `

${oeTab()}a[${oeTab()}b${oeTab()}

` + + `

${oeTab()}c${oeTab()}d${oeTab()}

` + + `
${oeTab()}e${oeTab()}]f
` + + `

zzz

`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfterEdit: `

xxx

` + + `

a[${oeTab(32.8906, false)}b${oeTab(32, false)}

` + + `

c${oeTab(25.7969, false)}d${oeTab(22.2031, false)}

` + + `
e${oeTab(32.8906, false)}]f
` + + `

zzz

`, + contentAfter: `

xxx

` + + `

a[${oeTab(32.8906)}b${oeTab(32)}

` + + `

c${oeTab(25.7969)}d${oeTab(22.2031)}

` + + `
e${oeTab(32.8906)}]f
` + + `

zzz

`, + }); + }); + it('should remove tab characters from the beginning of blocks and outdent lists', async () => { + await testEditor(BasicEditor, { + contentBefore: `

${oeTab()}${oeTab()}a[${oeTab()}b${oeTab()}

` + + `` + + `
${oeTab()}f${oeTab()}]g
`, + stepFunction: editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab', shiftKey: true }), + contentAfterEdit: `

${oeTab(TAB_WIDTH, false)}a[${oeTab(32.8906, false)}b${oeTab(32, false)}

` + + `` + + `
f${oeTab(34.6719, false)}]g
`, + contentAfter: `

${oeTab(TAB_WIDTH)}a[${oeTab(32.8906)}b${oeTab(32)}

` + + `` + + `
f${oeTab(34.6719)}]g
`, + }); + }); + }); });