From 7a64a04421171469c58bb94bbe8c55b1e2fdf3f2 Mon Sep 17 00:00:00 2001 From: Antoine Guenet Date: Fri, 8 Jul 2022 09:42:36 +0000 Subject: [PATCH] [ADD] web_editor: add UI for tables This adds a menu to manipulate table rows and columns when hovering over them. Hovering over the first row shows the menu for the hovered column. Hovering over the first column shows the menu for the hovered row. The buttons in the menu allow the user to add/move/remove rows/columns. task-2858455 Part-of: odoo/odoo#92200 --- .../js/editor/odoo-editor/src/OdooEditor.js | 240 ++++++++++++++++- .../odoo-editor/src/commands/commands.js | 111 ++++++-- .../src/js/editor/odoo-editor/src/style.scss | 52 ++++ .../odoo-editor/test/spec/editor.test.js | 251 +++++++++++++++++- 4 files changed, 611 insertions(+), 43 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 c5ae7abc294..eb5a287519e 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 @@ -167,6 +167,16 @@ export const CLIPBOARD_WHITELISTS = { } }; +// Commands that don't require a DOM selection but take an argument instead. +const SELECTIONLESS_COMMANDS = [ + 'addRowAbove', + 'addRowBelow', + 'addColumnLeft', + 'addColumnRight', + 'removeRow', + 'removeColumn', +]; + function defaultOptions(defaultObject, object) { const newObject = Object.assign({}, defaultObject, object); for (const [key, value] of Object.entries(object)) { @@ -316,7 +326,11 @@ export class OdooEditor extends EventTarget { this._pluginCall('sanitizeElement', [editable]); - // Create the Powerbox's table picker. + // ------ + // Tables + // ------ + + // Create the table picker for the Powerbox. this.powerboxTablePicker = new TablePicker({ document: this.document, floating: true, @@ -328,7 +342,78 @@ export class OdooEditor extends EventTarget { colNumber: ev.detail.colNumber, }); }); - // Initialize the Powerbox. + // Create the table picker for the toolbar. + this.toolbarTablePicker = new TablePicker({ document: this.document }); + this.toolbarTablePicker.addEventListener('cell-selected', ev => { + this.execCommand('insertTable', { + rowNumber: ev.detail.rowNumber, + colNumber: ev.detail.colNumber, + }); + }); + // Create the table UI. + const parser = new DOMParser(); + for (const direction of ['row', 'column']) { + // Create the containers and the menu toggler. + const ui = parser.parseFromString(`
+
+ +
+
+
`, 'text/html').body.firstElementChild; + const uiMenu = ui.querySelector('.o_table_ui_menu'); + + // Create the move buttons. + if (direction === 'column') { + uiMenu.append(...parser.parseFromString(` +
Move left
+
Move right
+ `, 'text/html').body.children); + this.addDomListener(uiMenu.querySelector('.o_move_left'), 'click', this._onTableMoveLeftClick); + this.addDomListener(uiMenu.querySelector('.o_move_right'), 'click', this._onTableMoveRightClick); + } else { + uiMenu.append(...parser.parseFromString(` +
Move up
+
Move down
+ `, 'text/html').body.children); + this.addDomListener(uiMenu.querySelector('.o_move_up'), 'click', this._onTableMoveUpClick); + this.addDomListener(uiMenu.querySelector('.o_move_down'), 'click', this._onTableMoveDownClick); + } + + // Create the add buttons. + if (direction === 'column') { + uiMenu.append(...parser.parseFromString(` +
Insert left
+
Insert right
+ `, 'text/html').body.children); + this.addDomListener(uiMenu.querySelector('.o_insert_left'), 'click', () => this.execCommand('addColumnLeft', this._columnUiTarget)); + this.addDomListener(uiMenu.querySelector('.o_insert_right'), 'click', () => this.execCommand('addColumnRight', this._columnUiTarget)); + } else { + uiMenu.append(...parser.parseFromString(` +
Insert above
+
Insert below
+ `, 'text/html').body.children); + this.addDomListener(uiMenu.querySelector('.o_insert_above'), 'click', () => this.execCommand('addRowAbove', this._rowUiTarget)); + this.addDomListener(uiMenu.querySelector('.o_insert_below'), 'click', () => this.execCommand('addRowBelow', this._rowUiTarget)); + } + + // Add the delete button. + if (direction === 'column') { + uiMenu.append(parser.parseFromString(`
Delete
`, 'text/html').body.firstChild) + this.addDomListener(uiMenu.querySelector('.o_delete_column'), 'click', this._onTableDeleteColumnClick); + } else { + uiMenu.append(parser.parseFromString(`
Delete
`, 'text/html').body.firstChild) + this.addDomListener(uiMenu.querySelector('.o_delete_row'), 'click', this._onTableDeleteRowClick); + } + + this[`_${direction}Ui`] = ui; + this.document.body.append(ui); + this.addDomListener(ui.querySelector('.o_table_ui_menu_toggler'), 'click', this._onTableMenuTogglerClick); + } + + // -------- + // Powerbox + // -------- + let beforeStepIndex; this.powerbox = new Powerbox({ editable: this.editable, @@ -493,14 +578,6 @@ export class OdooEditor extends EventTarget { ], }); - this.toolbarTablePicker = new TablePicker({ document: this.document }); - this.toolbarTablePicker.addEventListener('cell-selected', ev => { - this.execCommand('insertTable', { - rowNumber: ev.detail.rowNumber, - colNumber: ev.detail.colNumber, - }); - }); - // ----------- // Bind events // ----------- @@ -522,6 +599,7 @@ export class OdooEditor extends EventTarget { this.addDomListener(this.document, 'mousedown', this._onDocumentMousedown); this.addDomListener(this.document, 'mouseup', this._onDocumentMouseup); this.addDomListener(this.document, 'mousemove', this._onDocumentMousemove); + this.addDomListener(this.document, 'click', this._onDocumentClick); this.multiselectionRefresh = this.multiselectionRefresh.bind(this); this._resizeObserver = new ResizeObserver(this.multiselectionRefresh); @@ -593,6 +671,9 @@ export class OdooEditor extends EventTarget { clearInterval(this._snapshotInterval); this._pluginCall('destroy', []); this.isDestroyed = true; + // Remove table UI + this._rowUi.remove(); + this._columnUi.remove(); } sanitize() { @@ -1764,8 +1845,10 @@ export class OdooEditor extends EventTarget { _applyRawCommand(method, ...args) { const sel = this.document.getSelection(); if ( - !this.editable.contains(sel.anchorNode) || - (sel.anchorNode !== sel.focusNode && !this.editable.contains(sel.focusNode)) + !(SELECTIONLESS_COMMANDS.includes(method) && args.length) && ( + !this.editable.contains(sel.anchorNode) || + (sel.anchorNode !== sel.focusNode && !this.editable.contains(sel.focusNode)) + ) ) { // Do not apply commands out of the editable area. return false; @@ -2154,6 +2237,58 @@ export class OdooEditor extends EventTarget { } } } + /** + * Show/hide and position the table row/column manipulation UI. + * + * @private + * @param {HTMLTableRowElement} [row=false] + * @param {HTMLTableCellElement} [column=false] + */ + _toggleTableUi(row=false, column=false) { + if (row) { + this._rowUi.style.visibility = 'visible'; + this._rowUiTarget = row; + this._positionTableUi(row); + } else { + this._rowUi.style.visibility = 'hidden'; + } + if (column) { + this._columnUi.style.visibility = 'visible'; + this._columnUiTarget = column; + this._positionTableUi(column); + } else { + this._columnUi.style.visibility = 'hidden'; + } + } + /** + * Position the table row/column tools (depending on whether a row or a cell + * is passed as argument). + * + * @private + * @param {HTMLTableRowElement|HTMLTableCellElement} element + */ + _positionTableUi(element) { + const isRow = element.nodeName === 'TR'; + const ui = isRow ? this._rowUi : this._columnUi; + const elementRect = element.getBoundingClientRect(); + const tableRect = closestElement(element, 'table').getBoundingClientRect(); + const wrappedUi = ui.firstElementChild; + const togglerRect = ui.querySelector('.o_table_ui_menu_toggler').getBoundingClientRect(); + const props = { + xy: {left: 'x', top: 'y'}, + size: {left: 'width', top: 'height'} + }; + + const side1 = isRow ? 'left' : 'top'; + ui.style[side1] = (isRow ? elementRect : tableRect)[props.xy[side1]] - togglerRect[props.size[side1]] + 'px'; + wrappedUi.style[side1] = (togglerRect[props.size[side1]] / 2) + 'px'; + ui.style[props.size[side1]] = togglerRect[props.size[side1]] + 'px'; + + const side2 = isRow ? 'top' : 'left'; + ui.style[side2] = elementRect[props.xy[side2]] + 'px'; + wrappedUi.style[side2] = (elementRect[props.size[side2]] / 2) - (togglerRect[props.size[side2]] / 2) + 'px'; + ui.style[props.size[side2]] = elementRect[props.size[side2]] + 'px'; + } // HISTORY // ======= @@ -3514,12 +3649,29 @@ export class OdooEditor extends EventTarget { if (this._currentMouseState === 'mousedown' && !this._isResizingTable) { this._handleSelectionInTable(ev); } + if (!this._rowUi.classList.contains('o_open') && !this._columnUi.classList.contains('o_open')) { + const column = closestElement(ev.target, 'td'); + if (this._isResizingTable || !column || !ev.target || ev.target.nodeType !== Node.ELEMENT_NODE) { + this._toggleTableUi(false, false); + } else { + const row = closestElement(column, 'tr'); + const isFirstColumn = column === row.querySelector('td'); + const isFirstRow = row === closestElement(column, 'table').querySelector('tr'); + this._toggleTableUi(isFirstColumn && row, isFirstRow && column); + } + } const direction = {top: 'row', right: 'col', bottom: 'row', left: 'col'}[this._isHoveringTdBorder(ev)] || false; if (direction || !this._isResizingTable) { this._toggleTableResizeCursor(direction); } } + _onDocumentClick(ev) { + // Close Table UI. + this._rowUi.classList.remove('o_open'); + this._columnUi.classList.remove('o_open'); + } + /** * Create a Link in the node text based on the given data * @@ -3796,6 +3948,70 @@ export class OdooEditor extends EventTarget { this._onTabulationInTable(ev); } } + _onTableMenuTogglerClick(ev) { + const uiWrapper = closestElement(ev.target, '.o_table_ui'); + uiWrapper.classList.toggle('o_open'); + if (uiWrapper.classList.contains('o_column_ui')) { + const columnIndex = getColumnIndex(this._columnUiTarget); + uiWrapper.querySelector('.o_move_left').classList.toggle('o_hide', columnIndex === 0); + const shouldHideRight = columnIndex === [...this._columnUiTarget.parentElement.children].filter(child => child.nodeName === 'TD').length - 1; + uiWrapper.querySelector('.o_move_right').classList.toggle('o_hide', shouldHideRight); + } else { + const rowIndex = getRowIndex(this._rowUiTarget); + uiWrapper.querySelector('.o_move_up').classList.toggle('o_hide', rowIndex === 0); + const shouldHideDown = rowIndex === [...this._rowUiTarget.parentElement.children].filter(child => child.nodeName === 'TR').length - 1; + uiWrapper.querySelector('.o_move_down').classList.toggle('o_hide', shouldHideDown); + } + ev.stopPropagation(); + } + _onTableMoveUpClick() { + if (this._rowUiTarget.previousSibling) { + this._rowUiTarget.previousSibling.before(this._rowUiTarget); + } + } + _onTableMoveDownClick() { + if (this._rowUiTarget.nextSibling) { + this._rowUiTarget.nextSibling.after(this._rowUiTarget); + } + } + _onTableMoveRightClick() { + const trs = [...this._columnUiTarget.parentElement.parentElement.children].filter(child => child.nodeName === 'TR'); + const columnIndex = getColumnIndex(this._columnUiTarget); + const tdsToMove = trs.map(tr => [...tr.children].filter(child => child.nodeName === 'TD')[columnIndex]); + for (const tdToMove of tdsToMove) { + const target = [...tdToMove.parentElement.children].filter(child => child.nodeName === 'TD')[columnIndex + 1]; + target.after(tdToMove); + } + } + _onTableMoveLeftClick() { + const trs = [...this._columnUiTarget.parentElement.parentElement.children].filter(child => child.nodeName === 'TR'); + const columnIndex = getColumnIndex(this._columnUiTarget); + const tdsToMove = trs.map(tr => [...tr.children].filter(child => child.nodeName === 'TD')[columnIndex]); + for (const tdToMove of tdsToMove) { + const target = [...tdToMove.parentElement.children].filter(child => child.nodeName === 'TD')[columnIndex - 1]; + target.before(tdToMove); + } + } + _onTableDeleteColumnClick() { + this.historyPauseSteps(); + const rows = [...closestElement(this._columnUiTarget, 'tr').parentElement.children].filter(child => child.nodeName === 'TR'); + this.execCommand('removeColumn', this._columnUiTarget); + if (rows.every(row => !row.parentElement)) { + this.execCommand('deleteTable', this.editable.querySelector('.o_selected_table')); + } + this.historyUnpauseSteps(); + this.historyStep(); + } + _onTableDeleteRowClick() { + this.historyPauseSteps(); + const rows = [...this._rowUiTarget.parentElement.children].filter(child => child.nodeName === 'TR'); + this.execCommand('removeRow', this._rowUiTarget); + if (rows.every(row => !row.parentElement)) { + this.execCommand('deleteTable', this.editable.querySelector('.o_selected_table')); + } + this.historyUnpauseSteps(); + this.historyStep(); + } /** * Fix the current selection range in case the range start or end inside a fontAwesome node diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js index fece7d19fc9..ad847413fa9 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js @@ -42,6 +42,9 @@ import { isUnbreakable, makeContentsInline, unwrapContents, + getColumnIndex, + pxToFloat, + getRowIndex, } from '../utils/utils.js'; const TEXT_CLASSES_REGEX = /\btext-[^\s]*\b/g; @@ -540,26 +543,72 @@ export function toggleFormat(editor, format) { } return changedElements; } -function addColumn(editor, beforeOrAfter) { - getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td. - const c = getInSelection(editor.document, 'td'); - if (!c) return; - const i = [...closestElement(c, 'tr').querySelectorAll('th, td')].findIndex(td => td === c); - const column = closestElement(c, 'table').querySelectorAll(`tr td:nth-of-type(${i + 1})`); - column.forEach(row => row[beforeOrAfter](document.createElement('td'))); +function addColumn(editor, beforeOrAfter, referenceCell) { + if (!referenceCell) { + getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td. + referenceCell = getInSelection(editor.document, 'td'); + if (!referenceCell) return; + } + const columnIndex = getColumnIndex(referenceCell); + const table = closestElement(referenceCell, 'table'); + const tableWidth = table.style.width ? pxToFloat(table.style.width) : table.clientWidth; + const referenceColumn = table.querySelectorAll(`tr td:nth-of-type(${columnIndex + 1})`); + const referenceCellWidth = referenceCell.style.width ? pxToFloat(referenceCell.style.width) : referenceCell.clientWidth; + // Temporarily set widths so proportions are respected. + const firstRow = table.querySelector('tr'); + const firstRowCells = [...firstRow.children].filter(child => child.nodeName === 'TD' || child.nodeName === 'TH'); + let totalWidth = 0; + for (const cell of firstRowCells) { + const width = cell.style.width ? pxToFloat(cell.style.width) : cell.clientWidth; + cell.style.width = width + 'px'; + // Spread the widths to preserve proportions. + // -1 for the width of the border of the new column. + const newWidth = Math.max(Math.round((width * tableWidth) / (tableWidth + referenceCellWidth - 1)), 13); + cell.style.width = newWidth + 'px'; + totalWidth += newWidth; + } + referenceColumn.forEach((cell, rowIndex) => { + const newCell = document.createElement('td'); + newCell.append(document.createElement('br')); + cell[beforeOrAfter](newCell); + if (rowIndex === 0) { + newCell.style.width = cell.style.width; + totalWidth += pxToFloat(cell.style.width); + } + }); + if (totalWidth !== tableWidth - 1) { // -1 for the width of the border of the new column. + firstRowCells[firstRowCells.length - 1].style.width = pxToFloat(firstRowCells[firstRowCells.length - 1].style.width) + (tableWidth - totalWidth - 1) + 'px'; + } + // Fix the table and row's width so it doesn't change. + table.style.width = tableWidth + 'px'; } -function addRow(editor, beforeOrAfter) { - getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr. - const row = getInSelection(editor.document, 'tr'); - if (!row) return; +function addRow(editor, beforeOrAfter, referenceRow) { + if (!referenceRow) { + getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr. + referenceRow = getInSelection(editor.document, 'tr'); + if (!referenceRow) return; + } + const referenceRowHeight = referenceRow.style.height ? pxToFloat(referenceRow.style.height) : referenceRow.clientHeight; const newRow = document.createElement('tr'); - const cells = row.querySelectorAll('td'); + newRow.style.height = referenceRowHeight + 'px'; + const cells = referenceRow.querySelectorAll('td'); + const referenceRowWidths = [...cells].map(cell => cell.style.width || cell.clientWidth + 'px'); newRow.append(...Array.from(Array(cells.length)).map(() => { const td = document.createElement('td'); td.append(document.createElement('br')); return td; })); - row[beforeOrAfter](newRow); + referenceRow[beforeOrAfter](newRow); + newRow.style.height = referenceRowHeight + 'px'; + // Preserve the width of the columns (applied only on the first row). + if (getRowIndex(newRow) === 0) { + let columnIndex = 0; + for (const column of newRow.children) { + column.style.width = referenceRowWidths[columnIndex]; + cells[columnIndex].style.width = ''; + columnIndex++; + } + } } function deleteTable(editor, table) { table = table || getInSelection(editor.document, 'table'); @@ -880,22 +929,24 @@ export const editorCommands = { const [table] = editorCommands.insertHTML(editor, tableHtml); setCursorStart(table.querySelector('td')); }, - addColumnLeft: editor => { - addColumn(editor, 'before'); + addColumnLeft: (editor, referenceColumn) => { + return addColumn(editor, 'before', referenceColumn); }, - addColumnRight: editor => { - addColumn(editor, 'after'); + addColumnRight: (editor, referenceColumn) => { + return addColumn(editor, 'after', referenceColumn); }, - addRowAbove: editor => { - addRow(editor, 'before'); + addRowAbove: (editor, referenceRow) => { + return addRow(editor, 'before', referenceRow); }, - addRowBelow: editor => { - addRow(editor, 'after'); + addRowBelow: (editor, referenceRow) => { + return addRow(editor, 'after', referenceRow); }, - removeColumn: editor => { - getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td. - const cell = getInSelection(editor.document, 'td'); - if (!cell) return; + removeColumn: (editor, cell) => { + if (!cell) { + getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td. + cell = getInSelection(editor.document, 'td'); + if (!cell) return; + } const table = closestElement(cell, 'table'); const cells = [...closestElement(cell, 'tr').querySelectorAll('th, td')]; const index = cells.findIndex(td => td === cell); @@ -903,10 +954,12 @@ export const editorCommands = { table.querySelectorAll(`tr td:nth-of-type(${index + 1})`).forEach(td => td.remove()); siblingCell ? setSelection(...startPos(siblingCell)) : deleteTable(editor, table); }, - removeRow: editor => { - getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr. - const row = getInSelection(editor.document, 'tr'); - if (!row) return; + removeRow: (editor, row) => { + if (!row) { + getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr. + row = getInSelection(editor.document, 'tr'); + if (!row) return; + } const table = closestElement(row, 'table'); const rows = [...table.querySelectorAll('tr')]; const rowIndex = rows.findIndex(tr => tr === row); diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/style.scss b/addons/web_editor/static/src/js/editor/odoo-editor/src/style.scss index 201ffd2ec44..fe1cccb22c5 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/style.scss +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/style.scss @@ -34,6 +34,58 @@ cursor: pointer !important; } } +.o_table_ui { + background-color: transparent; + position: absolute; + z-index: 10; + padding: 0; + + &:hover { + visibility: visible !important; + } + > div { + position: absolute; + } + .o_table_ui_menu_toggler { + cursor: pointer; + background-color: white; + color: #6e727a; + border: 1px solid #C9CCD2; + border-radius: 5px; + padding: 2px 3px; + } + .o_table_ui_menu { + display: none; + cursor: pointer; + background-color: white; + width: fit-content; + border: 1px solid #C9CCD2; + padding: 5px 0; + white-space: nowrap; + + > div:hover { + background-color: #ebebeb; + } + span { + margin-right: 8px; + color: #6e727a; + } + div { + padding: 0 8px; + } + } + &.o_open { + visibility: visible !important; + + .o_table_ui_menu { + display: block; + + > div.o_hide { + display: none; + } + } + } +} .oe-floating { box-shadow: 0px 3px 18px rgba(0, 0, 0, .23); diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js index 9d47da9183e..4d54c0ca085 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js @@ -3920,12 +3920,259 @@ X[] }); describe('tables', () => { + describe('add children', () => { + describe('row', () => { + describe('above', async () => { + it('should add a row above the top row', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '
abcdef[]
', + stepFunction: async editor => editor.execCommand('addRowAbove'), + contentAfter: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '



abcdef[]
', + }); + }); + it('should add a row above the middle row', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef
abcdef[]
', + stepFunction: async editor => editor.execCommand('addRowAbove'), + contentAfter: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef



abcdef[]
', + }); + }); + }); + describe('below', () => { + it('should add a row below the bottom row', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '
abcdef[]
', + stepFunction: async editor => editor.execCommand('addRowBelow'), + contentAfter: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef[]



', + }); + }); + it('should add a row below the middle row', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef[]
abcdef
', + stepFunction: async editor => editor.execCommand('addRowBelow'), + contentAfter: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef[]



abcdef
', + }); + }); + }); + }); + describe('column', () => { + describe('left', () => { + it('should add a column left of the leftmost column', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
ab[]cdef
abcdef
', + stepFunction: async editor => editor.execCommand('addColumnLeft'), + contentAfter: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '

ab[]cdef

abcdef
', + }); + }); + it('should add a column left of the middle column', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef
abcd[]ef
abcdef
', + stepFunction: async editor => editor.execCommand('addColumnLeft'), + contentAfter: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
ab
cdef
ab
cd[]ef
ab
cdef
', + }); + }); + }); + describe('right', () => { + it('should add a column right of the rightmost column', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef[]
abcdef
', + stepFunction: async editor => editor.execCommand('addColumnRight'), + contentAfter: '' + + '' + + '' + + '' + + // size was slightly adjusted to + // preserve table width in view on + // fractional division results + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef[]
abcdef
', + }); + }); + it('should add a column right of the middle column', async () => { + await testEditor(BasicEditor, { + contentBefore: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcdef
abcd[]ef
abcdef
', + stepFunction: async editor => editor.execCommand('addColumnRight'), + contentAfter: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
abcd
ef
abcd[]
ef
abcd
ef
', + }); + }); + }); + }); + }); describe('tab', () => { it('should add a new row on press tab at the end of a table', async () => { await testEditor(BasicEditor, { - contentBefore: '
abcdef[]
', + contentBefore: '
abcdef[]
', stepFunction: async editor => triggerEvent(editor.editable, 'keydown', { key: 'Tab'}), - contentAfter: '
abcdef
[]


', + contentAfter: '
abcdef
[]


', }); }); });