diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js index e398f125..54d7d630 100644 --- a/src/commands/removecolumncommand.js +++ b/src/commands/removecolumncommand.js @@ -10,7 +10,8 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import TableWalker from '../tablewalker'; -import { getSelectionAffectedTableCells } from '../utils'; +import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils'; +import { findAncestor } from './utils'; /** * The remove column command. @@ -32,15 +33,12 @@ export default class RemoveColumnCommand extends Command { const firstCell = selectedCells[ 0 ]; if ( firstCell ) { - const table = firstCell.parent.parent; + const table = findAncestor( 'table', firstCell ); const tableColumnCount = this.editor.plugins.get( 'TableUtils' ).getColumns( table ); - const tableMap = [ ...new TableWalker( table ) ]; - const columnIndexes = tableMap.filter( entry => selectedCells.includes( entry.cell ) ).map( el => el.column ).sort(); - const minColumnIndex = columnIndexes[ 0 ]; - const maxColumnIndex = columnIndexes[ columnIndexes.length - 1 ]; + const { first, last } = getColumnIndexes( selectedCells ); - this.isEnabled = maxColumnIndex - minColumnIndex < ( tableColumnCount - 1 ); + this.isEnabled = last - first < ( tableColumnCount - 1 ); } else { this.isEnabled = false; } diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js index 641d5b25..e4737791 100644 --- a/src/commands/removerowcommand.js +++ b/src/commands/removerowcommand.js @@ -60,7 +60,10 @@ export default class RemoveRowCommand extends Command { const columnIndexToFocus = this.editor.plugins.get( 'TableUtils' ).getCellLocation( firstCell ).column; - model.change( writer => { + // Use single batch to modify table in steps but in one undo step. + const batch = model.createBatch(); + + model.enqueueChange( batch, writer => { // This prevents the "model-selection-range-intersects" error, caused by removing row selected cells. writer.setSelection( writer.createSelection( table, 'on' ) ); @@ -68,9 +71,12 @@ export default class RemoveRowCommand extends Command { this.editor.plugins.get( 'TableUtils' ).removeRows( table, { at: removedRowIndexes.first, - rows: rowsToRemove + rows: rowsToRemove, + batch } ); + } ); + model.enqueueChange( batch, writer => { const cellToFocus = getCellToFocus( table, removedRowIndexes.first, columnIndexToFocus ); writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) ); diff --git a/src/commands/setheadercolumncommand.js b/src/commands/setheadercolumncommand.js index 223c6079..69c9d125 100644 --- a/src/commands/setheadercolumncommand.js +++ b/src/commands/setheadercolumncommand.js @@ -9,11 +9,8 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { - updateNumericAttribute, - isHeadingColumnCell -} from './utils'; -import { getSelectionAffectedTableCells } from '../utils'; +import { findAncestor, isHeadingColumnCell, updateNumericAttribute } from './utils'; +import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils'; /** * The header column command. @@ -66,26 +63,19 @@ export default class SetHeaderColumnCommand extends Command { * the `forceValue` parameter instead of the current model state. */ execute( options = {} ) { - const model = this.editor.model; - const tableUtils = this.editor.plugins.get( 'TableUtils' ); - - const selectedCells = getSelectionAffectedTableCells( model.document.selection ); - const firstCell = selectedCells[ 0 ]; - const lastCell = selectedCells[ selectedCells.length - 1 ]; - const tableRow = firstCell.parent; - const table = tableRow.parent; - - const [ selectedColumnMin, selectedColumnMax ] = - // Returned cells might not necessary be in order, so make sure to sort it. - [ tableUtils.getCellLocation( firstCell ).column, tableUtils.getCellLocation( lastCell ).column ].sort(); - if ( options.forceValue === this.value ) { return; } - const headingColumnsToSet = this.value ? selectedColumnMin : selectedColumnMax + 1; + const model = this.editor.model; + const selectedCells = getSelectionAffectedTableCells( model.document.selection ); + const { first, last } = getColumnIndexes( selectedCells ); + + const headingColumnsToSet = this.value ? first : last + 1; model.change( writer => { + const table = findAncestor( 'table', selectedCells[ 0 ] ); + updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 ); } ); } diff --git a/src/commands/setheaderrowcommand.js b/src/commands/setheaderrowcommand.js index c318c887..a4fc9680 100644 --- a/src/commands/setheaderrowcommand.js +++ b/src/commands/setheaderrowcommand.js @@ -9,8 +9,8 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; -import { createEmptyTableCell, updateNumericAttribute } from './utils'; -import { getSelectionAffectedTableCells } from '../utils'; +import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './utils'; +import { getRowIndexes, getSelectionAffectedTableCells } from '../utils'; import TableWalker from '../tablewalker'; /** @@ -62,24 +62,16 @@ export default class SetHeaderRowCommand extends Command { * the `forceValue` parameter instead of the current model state. */ execute( options = {} ) { - const model = this.editor.model; - - const selectedCells = getSelectionAffectedTableCells( model.document.selection ); - const firstCell = selectedCells[ 0 ]; - const lastCell = selectedCells[ selectedCells.length - 1 ]; - const table = firstCell.parent.parent; - - const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0; - - const [ selectedRowMin, selectedRowMax ] = - // Returned cells might not necessary be in order, so make sure to sort it. - [ firstCell.parent.index, lastCell.parent.index ].sort(); - if ( options.forceValue === this.value ) { return; } + const model = this.editor.model; + const selectedCells = getSelectionAffectedTableCells( model.document.selection ); + const table = findAncestor( 'table', selectedCells[ 0 ] ); - const headingRowsToSet = this.value ? selectedRowMin : selectedRowMax + 1; + const { first, last } = getRowIndexes( selectedCells ); + const headingRowsToSet = this.value ? first : last + 1; + const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0; model.change( writer => { if ( headingRowsToSet ) { diff --git a/src/converters/downcast.js b/src/converters/downcast.js index a32f8998..cbd9ad0a 100644 --- a/src/converters/downcast.js +++ b/src/converters/downcast.js @@ -209,9 +209,6 @@ export function downcastTableHeadingRowsChange( options = {} ) { renameViewTableCell( tableCell, 'th', conversionApi, asWidget ); } } - - // Cleanup: this will remove any empty section from the view which may happen when moving all rows from a table section. - removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi ); } // The head section has shrunk so move rows from to . else { @@ -234,11 +231,12 @@ export function downcastTableHeadingRowsChange( options = {} ) { for ( const tableWalkerValue of tableWalker ) { renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conversionApi, asWidget ); } - - // Cleanup: this will remove any empty section from the view which may happen when moving all rows from a table section. - removeTableSectionIfEmpty( 'thead', viewTable, conversionApi ); } + // Cleanup: Ensure that thead & tbody sections are removed if left empty after moving rows. See #6437, #6391. + removeTableSectionIfEmpty( 'thead', viewTable, conversionApi ); + removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi ); + function isBetween( index, lower, upper ) { return index > lower && index < upper; } @@ -298,6 +296,7 @@ export function downcastRemoveRow() { const viewStart = mapper.toViewPosition( data.position ).getLastMatchingPosition( value => !value.item.is( 'tr' ) ); const viewItem = viewStart.nodeAfter; const tableSection = viewItem.parent; + const viewTable = tableSection.parent; // Remove associated from the view. const removeRange = viewWriter.createRangeOn( viewItem ); @@ -307,11 +306,9 @@ export function downcastRemoveRow() { mapper.unbindViewElement( child ); } - // Check if table section has any children left - if not remove it from the view. - if ( !tableSection.childCount ) { - // No need to unbind anything as table section is not represented in the model. - viewWriter.remove( viewWriter.createRangeOn( tableSection ) ); - } + // Cleanup: Ensure that thead & tbody sections are removed if left empty after removing rows. See #6437, #6391. + removeTableSectionIfEmpty( 'thead', viewTable, conversionApi ); + removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi ); }, { priority: 'higher' } ); } diff --git a/src/tableutils.js b/src/tableutils.js index a1e1b17b..91f50ee7 100644 --- a/src/tableutils.js +++ b/src/tableutils.js @@ -267,13 +267,13 @@ export default class TableUtils extends Plugin { * ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐ * 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0 * │ ├───┼───┤ │ ├───┼───┤ - * 1 │ │ d │ e │ <-- remove from here │ │ h │ i │ 1 - * │ ├───┼───┤ will give: ├───┼───┼───┤ - * 2 │ │ f │ g │ │ j │ k │ l │ 2 - * │ ├───┼───┤ └───┴───┴───┘ - * 3 │ │ h │ i │ + * 1 │ │ d │ e │ <-- remove from here │ │ d │ g │ 1 + * │ │ ├───┤ will give: ├───┼───┼───┤ + * 2 │ │ │ f │ │ h │ i │ j │ 2 + * │ │ ├───┤ └───┴───┴───┘ + * 3 │ │ │ g │ * ├───┼───┼───┤ - * 4 │ j │ k │ l │ + * 4 │ h │ i │ j │ * └───┴───┴───┘ * * @param {module:engine/model/element~Element} table @@ -283,27 +283,36 @@ export default class TableUtils extends Plugin { */ removeRows( table, options ) { const model = this.editor.model; - const first = options.at; - const rowsToRemove = options.rows || 1; + const rowsToRemove = options.rows || 1; + const first = options.at; const last = first + rowsToRemove - 1; + const batch = options.batch || 'default'; - model.change( writer => { - for ( let i = last; i >= first; i-- ) { - removeRow( table, i, writer ); - } + // Removing rows from table requires most calculations to be done prior to changing table structure. - const headingRows = table.getAttribute( 'headingRows' ) || 0; + // 1. Preparation - get row-spanned cells that have to be modified after removing rows. + const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last ); - if ( headingRows && first < headingRows ) { - const newRows = getNewHeadingRowsValue( first, last, headingRows ); + // 2. Execution + model.enqueueChange( batch, writer => { + // 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows. + // This will fill any gaps in a rows below that previously were empty because of row-spanned cells. + const rowAfterRemovedSection = last + 1; + moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer ); - // Must be done after the changes in table structure (removing rows). - // Otherwise the downcast converter for headingRows attribute will fail. ckeditor/ckeditor5#6391. - model.enqueueChange( writer.batch, writer => { - updateNumericAttribute( 'headingRows', newRows, table, writer, 0 ); - } ); + // 2b. Remove all required rows. + for ( let i = last; i >= first; i-- ) { + writer.remove( table.getChild( i ) ); + } + + // 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells. + for ( const { rowspan, cell } of cellsToTrim ) { + updateNumericAttribute( 'rowspan', rowspan, cell, writer ); } + + // 2d. Adjust heading rows if removed rows were in a heading section. + updateHeadingRows( table, first, last, model, batch ); } ); } @@ -730,60 +739,123 @@ function breakSpanEvenly( span, numberOfCells ) { return { newCellsSpan, updatedSpan }; } -function removeRow( table, rowIndex, writer ) { - const cellsToMove = new Map(); - const tableRow = table.getChild( rowIndex ); - const tableMap = [ ...new TableWalker( table, { endRow: rowIndex } ) ]; - - // Get cells from removed row that are spanned over multiple rows. - tableMap - .filter( ( { row, rowspan } ) => row === rowIndex && rowspan > 1 ) - .forEach( ( { column, cell, rowspan } ) => cellsToMove.set( column, { cell, rowspanToSet: rowspan - 1 } ) ); - - // Reduce rowspan on cells that are above removed row and overlaps removed row. - tableMap - .filter( ( { row, rowspan } ) => row <= rowIndex - 1 && row + rowspan > rowIndex ) - .forEach( ( { cell, rowspan } ) => updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer ) ); - - // Move cells to another row. - const targetRow = rowIndex + 1; - const tableWalker = new TableWalker( table, { includeSpanned: true, startRow: targetRow, endRow: targetRow } ); - let previousCell; +// Updates heading columns attribute if removing a row from head section. +function adjustHeadingColumns( table, removedColumnIndexes, writer ) { + const headingColumns = table.getAttribute( 'headingColumns' ) || 0; - for ( const { row, column, cell } of [ ...tableWalker ] ) { - if ( cellsToMove.has( column ) ) { - const { cell: cellToMove, rowspanToSet } = cellsToMove.get( column ); - const targetPosition = previousCell ? - writer.createPositionAfter( previousCell ) : - writer.createPositionAt( table.getChild( row ), 0 ); - writer.move( writer.createRangeOn( cellToMove ), targetPosition ); - updateNumericAttribute( 'rowspan', rowspanToSet, cellToMove, writer ); - previousCell = cellToMove; - } else { - previousCell = cell; - } - } + if ( headingColumns && removedColumnIndexes.first < headingColumns ) { + const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) - + removedColumnIndexes.first + 1; - writer.remove( tableRow ); + writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table ); + } } // Calculates a new heading rows value for removing rows from heading section. -function getNewHeadingRowsValue( first, last, headingRows ) { - if ( last < headingRows ) { - return headingRows - ( last - first + 1 ); +function updateHeadingRows( table, first, last, model, batch ) { + const headingRows = table.getAttribute( 'headingRows' ) || 0; + + if ( first < headingRows ) { + const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first; + + // Must be done after the changes in table structure (removing rows). + // Otherwise the downcast converter for headingRows attribute will fail. ckeditor/ckeditor5#6391. + model.enqueueChange( batch, writer => { + updateNumericAttribute( 'headingRows', newRows, table, writer, 0 ); + } ); } +} - return first; +// Finds cells that will be: +// - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed. +// - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section. +// +// Sample table with overlapping & sticking out cells: +// +// +----+----+----+----+----+ +// | 00 | 01 | 02 | 03 | 04 | +// +----+ + + + + +// | 10 | | | | | +// +----+----+ + + + +// | 20 | 21 | | | | <-- removed row +// + + +----+ + + +// | | | 32 | | | <-- removed row +// +----+ + +----+ + +// | 40 | | | 43 | | +// +----+----+----+----+----+ +// +// In a table above: +// - cells to trim: '02', '03' & '04'. +// - cells to move: '21' & '32'. +function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) { + const cellsToMove = new Map(); + const cellsToTrim = []; + + for ( const { row, column, rowspan, cell } of new TableWalker( table, { endRow: last } ) ) { + const lastRowOfCell = row + rowspan - 1; + + const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last; + + if ( isCellStickingOutFromRemovedRows ) { + const rowspanInRemovedSection = last - row + 1; + const rowSpanToSet = rowspan - rowspanInRemovedSection; + + cellsToMove.set( column, { + cell, + rowspan: rowSpanToSet + } ); + } + + const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first; + + if ( isCellOverlappingRemovedRows ) { + let rowspanAdjustment; + + // Cell fully covers removed section - trim it by removed rows count. + if ( lastRowOfCell >= last ) { + rowspanAdjustment = last - first + 1; + } + // Cell partially overlaps removed section - calculate cell's span that is in removed section. + else { + rowspanAdjustment = lastRowOfCell - first + 1; + } + + cellsToTrim.push( { + cell, + rowspan: rowspan - rowspanAdjustment + } ); + } + } + return { cellsToMove, cellsToTrim }; } -// Updates heading columns attribute if removing a row from head section. -function adjustHeadingColumns( table, removedColumnIndexes, writer ) { - const headingColumns = table.getAttribute( 'headingColumns' ) || 0; +function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) { + const tableWalker = new TableWalker( table, { + includeSpanned: true, + startRow: targetRowIndex, + endRow: targetRowIndex + } ); - if ( headingColumns && removedColumnIndexes.first < headingColumns ) { - const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) - - removedColumnIndexes.first + 1; + const tableRowMap = [ ...tableWalker ]; + const row = table.getChild( targetRowIndex ); - writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table ); + let previousCell; + + for ( const { column, cell, isSpanned } of tableRowMap ) { + if ( cellsToMove.has( column ) ) { + const { cell: cellToMove, rowspan } = cellsToMove.get( column ); + + const targetPosition = previousCell ? + writer.createPositionAfter( previousCell ) : + writer.createPositionAt( row, 0 ); + + writer.move( writer.createRangeOn( cellToMove ), targetPosition ); + updateNumericAttribute( 'rowspan', rowspan, cellToMove, writer ); + + previousCell = cellToMove; + } else if ( !isSpanned ) { + // If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502. + previousCell = cell; + } } } diff --git a/src/utils.js b/src/utils.js index 76317fdb..f5536c11 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,6 +9,7 @@ import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils'; import { findAncestor } from './commands/utils'; +import TableWalker from './tablewalker'; /** * Converts a given {@link module:engine/view/element~Element} to a table widget: @@ -138,24 +139,54 @@ export function getSelectionAffectedTableCells( selection ) { } /** - * Returns a helper object with `first` and `last` row index contained in given `tableCells`. + * Returns an object with `first` and `last` row index contained in the given `tableCells`. * * const selectedTableCells = getSelectedTableCells( editor.model.document.selection ); * * const { first, last } = getRowIndexes( selectedTableCells ); * - * console.log( `Selected rows ${ first } to ${ last }` ); + * console.log( `Selected rows: ${ first } to ${ last }` ); * - * @package {Array.} + * @param {Array.} tableCells * @returns {Object} Returns an object with `first` and `last` table row indexes. */ export function getRowIndexes( tableCells ) { - const allIndexesSorted = tableCells.map( cell => cell.parent.index ).sort(); + const indexes = tableCells.map( cell => cell.parent.index ); - return { - first: allIndexesSorted[ 0 ], - last: allIndexesSorted[ allIndexesSorted.length - 1 ] - }; + return getFirstLastIndexesObject( indexes ); +} + +/** + * Returns an object with `first` and `last` column index contained in the given `tableCells`. + * + * const selectedTableCells = getSelectedTableCells( editor.model.document.selection ); + * + * const { first, last } = getColumnIndexes( selectedTableCells ); + * + * console.log( `Selected columns: ${ first } to ${ last }` ); + * + * @param {Array.} tableCells + * @returns {Object} Returns an object with `first` and `last` table column indexes. + */ +export function getColumnIndexes( tableCells ) { + const table = findAncestor( 'table', tableCells[ 0 ] ); + const tableMap = [ ...new TableWalker( table ) ]; + + const indexes = tableMap + .filter( entry => tableCells.includes( entry.cell ) ) + .map( entry => entry.column ); + + return getFirstLastIndexesObject( indexes ); +} + +// Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes. +function getFirstLastIndexesObject( indexes ) { + const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB ); + + const first = allIndexesSorted[ 0 ]; + const last = allIndexesSorted[ allIndexesSorted.length - 1 ]; + + return { first, last }; } function sortRanges( rangesIterator ) { diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js index d0689ece..7200beda 100644 --- a/tests/commands/mergecellscommand.js +++ b/tests/commands/mergecellscommand.js @@ -208,6 +208,35 @@ describe( 'MergeCellsCommand', () => { expect( command.isEnabled ).to.be.false; } ); + + it( 'should be false if more than 10 rows selected and some are in heading section', () => { + setData( model, modelTable( [ + [ '0' ], + [ '1' ], + [ '2' ], + [ '3' ], + [ '4' ], + [ '5' ], + [ '6' ], + [ '7' ], + [ '8' ], + [ '9' ], + [ '10' ], + [ '11' ], + [ '12' ], + [ '13' ], + [ '14' ] + ], { headingRows: 10 } ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 0 ] ), + modelRoot.getNodeByPath( [ 0, 12, 0 ] ) + ); + + expect( command.isEnabled ).to.be.false; + } ); } ); describe( 'execute()', () => { diff --git a/tests/commands/removecolumncommand.js b/tests/commands/removecolumncommand.js index 487a2f16..d352e5b7 100644 --- a/tests/commands/removecolumncommand.js +++ b/tests/commands/removecolumncommand.js @@ -86,6 +86,21 @@ describe( 'RemoveColumnCommand', () => { expect( command.isEnabled ).to.be.false; } ); + it( 'should be false if all columns are selected - table with more than 10 columns (array sort bug)', () => { + setData( model, modelTable( [ + [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ] + ] ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 0, 12 ] ) + ); + + expect( command.isEnabled ).to.be.false; + } ); + it( 'should be false if selection is outside a table', () => { setData( model, '11[]' ); diff --git a/tests/commands/removerowcommand.js b/tests/commands/removerowcommand.js index 21ccb315..75fb74ae 100644 --- a/tests/commands/removerowcommand.js +++ b/tests/commands/removerowcommand.js @@ -9,22 +9,19 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils import RemoveRowCommand from '../../src/commands/removerowcommand'; import TableSelection from '../../src/tableselection'; -import { defaultConversion, defaultSchema, modelTable, viewTable } from '../_utils/utils'; +import { modelTable, viewTable } from '../_utils/utils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import TableEditing from '../../src/tableediting'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; describe( 'RemoveRowCommand', () => { let editor, model, command; - beforeEach( () => { - return VirtualTestEditor.create( { plugins: [ TableSelection ] } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; - command = new RemoveRowCommand( editor ); + beforeEach( async () => { + editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing, TableSelection ] } ); - defaultSchema( model.schema ); - defaultConversion( editor.conversion ); - } ); + model = editor.model; + command = new RemoveRowCommand( editor ); } ); afterEach( () => { @@ -105,6 +102,33 @@ describe( 'RemoveRowCommand', () => { expect( command.isEnabled ).to.be.false; } ); + + it( 'should be false if all the rows are selected - table with more than 10 rows (array sort bug)', () => { + setData( model, modelTable( [ + [ '0' ], + [ '1' ], + [ '2' ], + [ '3' ], + [ '4' ], + [ '5' ], + [ '6' ], + [ '7' ], + [ '8' ], + [ '9' ], + [ '10' ], + [ '11' ], + [ '12' ] + ] ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 0 ] ), + modelRoot.getNodeByPath( [ 0, 12, 0 ] ) + ); + + expect( command.isEnabled ).to.be.false; + } ); } ); describe( 'execute()', () => { @@ -263,11 +287,11 @@ describe( 'RemoveRowCommand', () => { [ '[]40', '41' ] ], { headingRows: 1 } ) ); - // The view should also be properly downcasted. + // The editing view should also be properly downcasted. assertEqualMarkup( getViewData( editor.editing.view, { withoutSelection: true } ), viewTable( [ [ '00', '01' ], [ '40', '41' ] - ], { headingRows: 1 } ) ); + ], { headingRows: 1, asWidget: true } ) ); } ); it( 'should support removing mixed heading and cell rows', () => { @@ -339,6 +363,41 @@ describe( 'RemoveRowCommand', () => { expect( createdBatches.size ).to.equal( 1 ); } ); + + it( 'should properly remove more than 10 rows selected (array sort bug)', () => { + setData( model, modelTable( [ + [ '0' ], + [ '1' ], + [ '2' ], + [ '3' ], + [ '4' ], + [ '5' ], + [ '6' ], + [ '7' ], + [ '8' ], + [ '9' ], + [ '10' ], + [ '11' ], + [ '12' ], + [ '13' ], + [ '14' ] + ] ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 0 ] ), + modelRoot.getNodeByPath( [ 0, 12, 0 ] ) + ); + + command.execute(); + + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '0' ], + [ '13' ], + [ '14' ] + ] ) ); + } ); } ); describe( 'with entire row selected', () => { @@ -447,8 +506,8 @@ describe( 'RemoveRowCommand', () => { setData( model, modelTable( [ [ { rowspan: 4, contents: '00' }, { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ], [ { rowspan: 2, contents: '13' }, '14' ], - [ '22[]', '23', '24' ], - [ '30', '31', '32', '33', '34' ] + [ '22[]', '24' ], + [ '31', '32', '33', '34' ] ] ) ); command.execute(); @@ -456,7 +515,7 @@ describe( 'RemoveRowCommand', () => { assertEqualMarkup( getData( model ), modelTable( [ [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ], [ '13', '14' ], - [ '30', '31', '[]32', '33', '34' ] + [ '31', '32', '[]33', '34' ] ] ) ); } ); diff --git a/tests/commands/selectrowcommand.js b/tests/commands/selectrowcommand.js index adcbc56a..a161d667 100644 --- a/tests/commands/selectrowcommand.js +++ b/tests/commands/selectrowcommand.js @@ -422,6 +422,53 @@ describe( 'SelectRowCommand', () => { [ 0, 0 ] ] ); } ); + + it( 'should properly select more than 10 rows selected (array sort bug)', () => { + setData( model, modelTable( [ + [ '0', 'x' ], + [ '1', 'x' ], + [ '2', 'x' ], + [ '3', 'x' ], + [ '4', 'x' ], + [ '5', 'x' ], + [ '6', 'x' ], + [ '7', 'x' ], + [ '8', 'x' ], + [ '9', 'x' ], + [ '10', 'x' ], + [ '11', 'x' ], + [ '12', 'x' ], + [ '13', 'x' ], + [ '14', 'x' ] + ] ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 1, 0 ] ), + modelRoot.getNodeByPath( [ 0, 12, 0 ] ) + ); + + command.execute(); + + assertSelectedCells( model, [ + [ 0, 0 ], // '0' + [ 1, 1 ], // '1' + [ 1, 1 ], // '2' + [ 1, 1 ], // '3' + [ 1, 1 ], // '4' + [ 1, 1 ], // '5' + [ 1, 1 ], // '6' + [ 1, 1 ], // '7' + [ 1, 1 ], // '8' + [ 1, 1 ], // '9' + [ 1, 1 ], // '10' + [ 1, 1 ], // '11' + [ 1, 1 ], // '12' + [ 0, 0 ], // '13' + [ 0, 0 ] // '14 + ] ); + } ); } ); describe( 'with entire row selected', () => { diff --git a/tests/commands/setheadercolumncommand.js b/tests/commands/setheadercolumncommand.js index 1e17d39c..8871ff77 100644 --- a/tests/commands/setheadercolumncommand.js +++ b/tests/commands/setheadercolumncommand.js @@ -364,6 +364,25 @@ describe( 'SetHeaderColumnCommand', () => { [ 0, 1, 1, 0 ] ] ); } ); + + it( 'should set it correctly in table with more than 10 columns (array sort bug)', () => { + setData( model, modelTable( [ + [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14' ] + ] ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 0, 2 ] ), + modelRoot.getNodeByPath( [ 0, 0, 13 ] ) + ); + + command.execute(); + + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14' ] + ], { headingColumns: 14 } ) ); + } ); } ); } ); diff --git a/tests/commands/setheaderrowcommand.js b/tests/commands/setheaderrowcommand.js index 87be0adf..b0f5ccf1 100644 --- a/tests/commands/setheaderrowcommand.js +++ b/tests/commands/setheaderrowcommand.js @@ -346,6 +346,100 @@ describe( 'SetHeaderRowCommand', () => { ] ); } ); + it( 'should set it correctly in table with more than 10 columns (array sort bug)', () => { + setData( model, modelTable( [ + [ '0', 'x' ], + [ '1', 'x' ], + [ '2', 'x' ], + [ '3', 'x' ], + [ '4', 'x' ], + [ '5', 'x' ], + [ '6', 'x' ], + [ '7', 'x' ], + [ '8', 'x' ], + [ '9', 'x' ], + [ '10', 'x' ], + [ '11', 'x' ], + [ '12', 'x' ], + [ '13', 'x' ], + [ '14', 'x' ] + ] ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 2, 0 ] ), + modelRoot.getNodeByPath( [ 0, 13, 0 ] ) + ); + + command.execute(); + + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '0', 'x' ], + [ '1', 'x' ], + [ '2', 'x' ], + [ '3', 'x' ], + [ '4', 'x' ], + [ '5', 'x' ], + [ '6', 'x' ], + [ '7', 'x' ], + [ '8', 'x' ], + [ '9', 'x' ], + [ '10', 'x' ], + [ '11', 'x' ], + [ '12', 'x' ], + [ '13', 'x' ], + [ '14', 'x' ] + ], { headingRows: 14 } ) ); + } ); + + it( 'should set it correctly in table with more than 10 columns (array sort bug, reversed selection)', () => { + setData( model, modelTable( [ + [ '0', 'x' ], + [ '1', 'x' ], + [ '2', 'x' ], + [ '3', 'x' ], + [ '4', 'x' ], + [ '5', 'x' ], + [ '6', 'x' ], + [ '7', 'x' ], + [ '8', 'x' ], + [ '9', 'x' ], + [ '10', 'x' ], + [ '11', 'x' ], + [ '12', 'x' ], + [ '13', 'x' ], + [ '14', 'x' ] + ] ) ); + + const tableSelection = editor.plugins.get( TableSelection ); + const modelRoot = model.document.getRoot(); + tableSelection._setCellSelection( + modelRoot.getNodeByPath( [ 0, 13, 0 ] ), + modelRoot.getNodeByPath( [ 0, 2, 1 ] ) + ); + + command.execute(); + + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '0', 'x' ], + [ '1', 'x' ], + [ '2', 'x' ], + [ '3', 'x' ], + [ '4', 'x' ], + [ '5', 'x' ], + [ '6', 'x' ], + [ '7', 'x' ], + [ '8', 'x' ], + [ '9', 'x' ], + [ '10', 'x' ], + [ '11', 'x' ], + [ '12', 'x' ], + [ '13', 'x' ], + [ '14', 'x' ] + ], { headingRows: 14 } ) ); + } ); + it( 'should remove header rows in case of multiple cell selection', () => { setData( model, modelTable( [ [ '00' ], diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js index 01695208..dbc79a9b 100644 --- a/tests/converters/downcast.js +++ b/tests/converters/downcast.js @@ -4,12 +4,14 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import { defaultConversion, defaultSchema, modelTable, viewTable } from '../_utils/utils'; +import TableEditing from '../../src/tableediting'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import { defaultConversion, defaultSchema, modelTable, viewTable } from '../_utils/utils'; function paragraphInTableCell() { return dispatcher => dispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => { @@ -41,21 +43,22 @@ describe( 'downcast converters', () => { testUtils.createSinonSandbox(); - beforeEach( () => { - return VirtualTestEditor.create() - .then( newEditor => { - editor = newEditor; - model = editor.model; - doc = model.document; - root = doc.getRoot( 'main' ); - view = editor.editing.view; - - defaultSchema( model.schema ); - defaultConversion( editor.conversion ); - } ); - } ); - describe( 'downcastInsertTable()', () => { + // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing. + beforeEach( () => { + return VirtualTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot( 'main' ); + view = editor.editing.view; + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should create table with tbody', () => { setModelData( model, modelTable( [ [ '' ] ] ) ); @@ -351,6 +354,21 @@ describe( 'downcast converters', () => { } ); describe( 'downcastInsertRow()', () => { + // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing. + beforeEach( () => { + return VirtualTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot( 'main' ); + view = editor.editing.view; + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should react to changed rows', () => { setModelData( model, modelTable( [ [ '00', '01' ] @@ -588,6 +606,21 @@ describe( 'downcast converters', () => { } ); describe( 'downcastInsertCell()', () => { + // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing. + beforeEach( () => { + return VirtualTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot( 'main' ); + view = editor.editing.view; + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should add tableCell on proper index in tr', () => { setModelData( model, modelTable( [ [ '00', '01' ] @@ -732,6 +765,21 @@ describe( 'downcast converters', () => { } ); describe( 'downcastTableHeadingColumnsChange()', () => { + // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing. + beforeEach( () => { + return VirtualTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot( 'main' ); + view = editor.editing.view; + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should work for adding heading columns', () => { setModelData( model, modelTable( [ [ '00', '01' ], @@ -891,7 +939,7 @@ describe( 'downcast converters', () => { assertEqualMarkup( getViewData( view, { withoutSelection: true } ), '
' + - '
' + + '
' + '' + '' + '' + @@ -908,190 +956,198 @@ describe( 'downcast converters', () => { } ); describe( 'downcastTableHeadingRowsChange()', () => { - it( 'should work for adding heading rows', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ] - ] ) ); + // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing. + beforeEach( () => { + return VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot( 'main' ); + view = editor.editing.view; + } ); + } ); - const table = root.getChild( 0 ); + // The heading rows change downcast conversion is not executed in data pipeline. + describe( 'editing pipeline', () => { + it( 'should work for adding heading rows', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ] + ] ) ); - model.change( writer => { - writer.setAttribute( 'headingRows', 2, table ); + const table = root.getChild( 0 ); + + model.change( writer => { + writer.setAttribute( 'headingRows', 2, table ); + } ); + + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ] + ], { headingRows: 2, asWidget: true } ) ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ] - ], { headingRows: 2 } ) ); - } ); + it( 'should work for changing number of heading rows to a bigger number', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ] + ], { headingRows: 1 } ) ); - it( 'should work for changing number of heading rows to a bigger number', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ] - ], { headingRows: 1 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + writer.setAttribute( 'headingRows', 2, table ); + } ); - model.change( writer => { - writer.setAttribute( 'headingRows', 2, table ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ] + ], { headingRows: 2, asWidget: true } ) ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ] - ], { headingRows: 2 } ) ); - } ); + it( 'should work for changing number of heading rows to a smaller number', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ], + [ '30', '31' ] + ], { headingRows: 3 } ) ); - it( 'should work for changing number of heading rows to a smaller number', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ], - [ '30', '31' ] - ], { headingRows: 3 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + writer.setAttribute( 'headingRows', 2, table ); + } ); - model.change( writer => { - writer.setAttribute( 'headingRows', 2, table ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ], + [ '30', '31' ] + ], { headingRows: 2, asWidget: true } ) ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ], - [ '30', '31' ] - ], { headingRows: 2 } ) ); - } ); + it( 'should work for removing heading rows', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ] + ], { headingRows: 2 } ) ); - it( 'should work for removing heading rows', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ] - ], { headingRows: 2 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + writer.removeAttribute( 'headingRows', table ); + } ); - model.change( writer => { - writer.removeAttribute( 'headingRows', table ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ + [ '00', '01' ], + [ '10', '11' ] + ], { asWidget: true } ) ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ], - [ '10', '11' ] - ] ) ); - } ); + it( 'should work for making heading rows without tbody', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ] + ] ) ); - it( 'should work for making heading rows without tbody', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ] - ] ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + writer.setAttribute( 'headingRows', 2, table ); + } ); - model.change( writer => { - writer.setAttribute( 'headingRows', 2, table ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ + [ '00', '01' ], + [ '10', '11' ] + ], { headingRows: 2, asWidget: true } ) ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ], - [ '10', '11' ] - ], { headingRows: 2 } ) ); - } ); + it( 'should be possible to overwrite', () => { + editor.conversion.attributeToAttribute( { + model: 'headingRows', + view: 'headingRows', + converterPriority: 'high' + } ); + setModelData( model, modelTable( [ [ '00' ] ] ) ); - it( 'should be possible to overwrite', () => { - editor.conversion.attributeToAttribute( { model: 'headingRows', view: 'headingRows', converterPriority: 'high' } ); - setModelData( model, modelTable( [ [ '00' ] ] ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + writer.setAttribute( 'headingRows', 1, table ); + } ); - model.change( writer => { - writer.setAttribute( 'headingRows', 1, table ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '
' + + '00' + + '
' + + '
' + ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), - '
' + - '' + - '' + - '' + - '' + - '
00
' + - '
' - ); - } ); + it( 'should work with adding table rows at the beginning of a table', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ] + ], { headingRows: 1 } ) ); - it( 'should work with adding table rows at the beginning of a table', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ] - ], { headingRows: 1 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + writer.setAttribute( 'headingRows', 2, table ); - model.change( writer => { - writer.setAttribute( 'headingRows', 2, table ); + const tableRow = writer.createElement( 'tableRow' ); - const tableRow = writer.createElement( 'tableRow' ); + writer.insert( tableRow, table, 0 ); + writer.insertElement( 'tableCell', tableRow, 'end' ); + writer.insertElement( 'tableCell', tableRow, 'end' ); + } ); - writer.insert( tableRow, table, 0 ); - writer.insertElement( 'tableCell', tableRow, 'end' ); - writer.insertElement( 'tableCell', tableRow, 'end' ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ + [ '', '' ], + [ '00', '01' ], + [ '10', '11' ] + ], { headingRows: 2, asWidget: true } ) ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '', '' ], - [ '00', '01' ], - [ '10', '11' ] - ], { headingRows: 2 } ) ); - } ); - - it( 'should work with adding a table row and expanding heading', () => { - setModelData( model, modelTable( [ - [ '00', '01' ], - [ '10', '11' ], - [ '20', '21' ] - ], { headingRows: 1 } ) ); - - const table = root.getChild( 0 ); - - model.change( writer => { - writer.setAttribute( 'headingRows', 2, table ); + it( 'should work with adding a table row and expanding heading', () => { + setModelData( model, modelTable( [ + [ '00', '01' ], + [ '10', '11' ], + [ '20', '21' ] + ], { headingRows: 1 } ) ); - const tableRow = writer.createElement( 'tableRow' ); + const table = root.getChild( 0 ); - writer.insert( tableRow, table, 1 ); - writer.insertElement( 'tableCell', tableRow, 'end' ); - writer.insertElement( 'tableCell', tableRow, 'end' ); - } ); + model.change( writer => { + writer.setAttribute( 'headingRows', 2, table ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ], - [ '', '' ], - [ '10', '11' ], - [ '20', '21' ] - ], { headingRows: 2 } ) ); - } ); + const tableRow = writer.createElement( 'tableRow' ); - describe( 'options.asWidget=true', () => { - beforeEach( () => { - return VirtualTestEditor.create() - .then( newEditor => { - editor = newEditor; - model = editor.model; - doc = model.document; - root = doc.getRoot( 'main' ); - view = editor.editing.view; + writer.insert( tableRow, table, 1 ); + writer.insertElement( 'tableCell', tableRow, 'end' ); + writer.insertElement( 'tableCell', tableRow, 'end' ); + } ); - defaultSchema( model.schema ); - defaultConversion( editor.conversion, true ); - } ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ + [ '00', '01' ], + [ '', '' ], + [ '10', '11' ], + [ '20', '21' ] + ], { headingRows: 2, asWidget: true } ) ); } ); it( 'should create renamed cell as a widget', () => { @@ -1122,107 +1178,254 @@ describe( 'downcast converters', () => { } ); describe( 'downcastRemoveRow()', () => { - it( 'should react to removed row from the beginning of a tbody', () => { - setModelData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ] ) ); + // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing. + beforeEach( async () => { + editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } ); - const table = root.getChild( 0 ); + model = editor.model; + root = model.document.getRoot( 'main' ); + view = editor.editing.view; + } ); - model.change( writer => { - writer.remove( table.getChild( 1 ) ); + // The remove row downcast conversion is not executed in data pipeline. + describe( 'editing pipeline', () => { + it( 'should react to removed row from the beginning of a body rows (no heading rows)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ] ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.remove( table.getChild( 1 ) ); + } ); + + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '00' + + '' + + '01' + + '
' + + '
' + ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ] - ] ) ); - } ); + it( 'should react to removed row form the end of a body rows (no heading rows)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ] ) ); - it( 'should react to removed row form the end of a tbody', () => { - setModelData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ] ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + writer.remove( table.getChild( 0 ) ); + } ); - model.change( writer => { - writer.remove( table.getChild( 0 ) ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '10' + + '' + + '11' + + '
' + + '
' + ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '10', '11' ] - ] ) ); - } ); + it( 'should react to removed row from the beginning of a heading rows (no body rows)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ], { headingRows: 2 } ) ); - it( 'should react to removed row from the beginning of a thead', () => { - setModelData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ], { headingRows: 2 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + // Removing row from a heading section changes requires changing heading rows attribute. + writer.setAttribute( 'headingRows', 1, table ); + writer.remove( table.getChild( 0 ) ); + } ); - model.change( writer => { - writer.remove( table.getChild( 1 ) ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '10' + + '' + + '11' + + '
' + + '
' + ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ] - ], { headingRows: 2 } ) ); - } ); + it( 'should react to removed row form the end of a heading rows (no body rows)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ], { headingRows: 2 } ) ); - it( 'should react to removed row form the end of a thead', () => { - setModelData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ], { headingRows: 2 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + // Removing row from a heading section changes requires changing heading rows attribute. + writer.setAttribute( 'headingRows', 1, table ); + writer.remove( table.getChild( 1 ) ); + } ); - model.change( writer => { - writer.remove( table.getChild( 0 ) ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '00' + + '' + + '01' + + '
' + + '
' + ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '10', '11' ] - ], { headingRows: 2 } ) ); - } ); + it( 'should react to removed row form the end of a heading rows (first cell in body has colspan)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01', '02', '03' ], + [ { rowspan: 2, colspan: 2, contents: '10' }, '12', '13' ], + [ '22', '23' ] + ], { headingRows: 1 } ) ); - it( 'should remove empty thead section if a last row was removed from thead', () => { - setModelData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ], { headingRows: 1 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + // Removing row from a heading section changes requires changing heading rows attribute. + writer.remove( table.getChild( 0 ) ); + writer.setAttribute( 'headingRows', 0, table ); + } ); - model.change( writer => { - writer.setAttribute( 'headingRows', 0, table ); - writer.remove( table.getChild( 0 ) ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '10' + + '' + + '12' + + '' + + '13' + + '
' + + '22' + + '' + + '23' + + '
' + + '
' + ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '10', '11' ] - ] ) ); - } ); + it( 'should remove empty thead if a last row was removed from a heading rows (has heading and body)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ], { headingRows: 1 } ) ); - it( 'should remove empty tbody section if a last row was removed from tbody', () => { - setModelData( model, modelTable( [ - [ '00[]', '01' ], - [ '10', '11' ] - ], { headingRows: 1 } ) ); + const table = root.getChild( 0 ); - const table = root.getChild( 0 ); + model.change( writer => { + // Removing row from a heading section changes requires changing heading rows attribute. + writer.removeAttribute( 'headingRows', table ); + writer.remove( table.getChild( 0 ) ); + } ); - model.change( writer => { - writer.remove( table.getChild( 1 ) ); + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '10' + + '' + + '11' + + '
' + + '
' + ); } ); - assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [ - [ '00', '01' ] - ], { headingRows: 1 } ) ); + it( 'should remove empty tbody if a last row was removed a body rows (has heading and body)', () => { + setModelData( model, modelTable( [ + [ '00[]', '01' ], + [ '10', '11' ] + ], { headingRows: 1 } ) ); + + const table = root.getChild( 0 ); + + model.change( writer => { + writer.remove( table.getChild( 1 ) ); + } ); + + assertEqualMarkup( getViewData( view, { withoutSelection: true } ), + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '00' + + '' + + '01' + + '
' + + '
' + ); + } ); } ); } ); diff --git a/tests/tableutils.js b/tests/tableutils.js index ae1c4f56..870412b1 100644 --- a/tests/tableutils.js +++ b/tests/tableutils.js @@ -8,37 +8,53 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model import { defaultConversion, defaultSchema, modelTable } from './_utils/utils'; +import TableEditing from '../src/tableediting'; import TableUtils from '../src/tableutils'; import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; describe( 'TableUtils', () => { let editor, model, root, tableUtils; - beforeEach( () => { - return ModelTestEditor.create( { - plugins: [ TableUtils ] - } ).then( newEditor => { - editor = newEditor; - model = editor.model; - root = model.document.getRoot( 'main' ); - tableUtils = editor.plugins.get( TableUtils ); - - defaultSchema( model.schema ); - defaultConversion( editor.conversion ); - } ); - } ); - afterEach( () => { return editor.destroy(); } ); describe( '#pluginName', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should provide plugin name', () => { expect( TableUtils.pluginName ).to.equal( 'TableUtils' ); } ); } ); describe( 'getCellLocation()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should return proper table cell location', () => { setData( model, modelTable( [ [ { rowspan: 2, colspan: 2, contents: '00[]' }, '02' ], @@ -52,6 +68,20 @@ describe( 'TableUtils', () => { } ); describe( 'insertRows()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should insert row in given table at given index', () => { setData( model, modelTable( [ [ '11[]', '12' ], @@ -192,6 +222,20 @@ describe( 'TableUtils', () => { } ); describe( 'insertColumns()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should insert column in given table at given index', () => { setData( model, modelTable( [ [ '11[]', '12' ], @@ -370,7 +414,7 @@ describe( 'TableUtils', () => { ], { headingColumns: 4 } ) ); } ); - it( 'should properly insert column while table has rowspanned cells', () => { + it( 'should properly insert column while table has row-spanned cells', () => { setData( model, modelTable( [ [ { rowspan: 4, contents: '00[]' }, { rowspan: 2, contents: '01' }, '02' ], [ '12' ], @@ -390,6 +434,20 @@ describe( 'TableUtils', () => { } ); describe( 'splitCellVertically()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should split table cell to given table cells number', () => { setData( model, modelTable( [ [ '00', '01', '02' ], @@ -534,6 +592,20 @@ describe( 'TableUtils', () => { } ); describe( 'splitCellHorizontally()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should split table cell to default table cells number', () => { setData( model, modelTable( [ [ '00', '01', '02' ], @@ -570,7 +642,7 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should properly update rowspanned cells overlapping selected cell', () => { + it( 'should properly update row-spanned cells overlapping selected cell', () => { setData( model, modelTable( [ [ { rowspan: 2, contents: '00' }, '01', { rowspan: 3, contents: '02' } ], [ '[]11' ], @@ -588,7 +660,7 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should split rowspanned cell', () => { + it( 'should split row-spanned cell', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, contents: '01[]' } ], [ '10' ], @@ -606,7 +678,7 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should copy colspan while splitting rowspanned cell', () => { + it( 'should copy colspan while splitting row-spanned cell', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, colspan: 2, contents: '01[]' } ], [ '10' ], @@ -652,7 +724,7 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should split rowspanned cell and updated other cells rowspan when splitting to bigger number of cells', () => { + it( 'should split row-spanned cell and updated other cells rowspan when splitting to bigger number of cells', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, contents: '01[]' } ], [ '10' ], @@ -671,7 +743,7 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should split rowspanned & colspaned cell', () => { + it( 'should split row-spanned & col-spanned cell', () => { setData( model, modelTable( [ [ '00', { colspan: 2, contents: '01[]' } ], [ '10', '11' ] @@ -709,6 +781,20 @@ describe( 'TableUtils', () => { } ); describe( 'getColumns()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should return proper number of columns', () => { setData( model, modelTable( [ [ '00', { colspan: 3, contents: '01' }, '04' ] @@ -719,6 +805,20 @@ describe( 'TableUtils', () => { } ); describe( 'getRows()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + it( 'should return proper number of columns for simple table', () => { setData( model, modelTable( [ [ '00', '01' ], @@ -749,6 +849,17 @@ describe( 'TableUtils', () => { } ); describe( 'removeRows()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ Paragraph, TableEditing, TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + } ); + } ); + describe( 'single row', () => { it( 'should remove a given row from a table start', () => { setData( model, modelTable( [ @@ -794,11 +905,57 @@ describe( 'TableUtils', () => { } ); it( 'should decrease rowspan of table cells from previous rows', () => { + // +----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | + // +----+ + + + + + // | 10 | | | | | + // +----+----+ + + + + // | 20 | 21 | | | | + // +----+----+----+ + + + // | 30 | 31 | 32 | | | + // +----+----+----+----+ + + // | 40 | 41 | 42 | 43 | | + // +----+----+----+----+----+ + // | 50 | 51 | 52 | 53 | 54 | + // +----+----+----+----+----+ + setData( model, modelTable( [ + [ '00', { contents: '01', rowspan: 2 }, { contents: '02', rowspan: 3 }, { contents: '03', rowspan: 4 }, + { contents: '04', rowspan: 5 } ], + [ '10' ], + [ '20', '21' ], + [ '30', '31', '32' ], + [ '40', '41', '42', '43' ], + [ '50', '51', '52', '53', '54' ] + ] ) ); + + tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 1 } ); + + // +----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | + // +----+----+ + + + + // | 20 | 21 | | | | + // +----+----+----+ + + + // | 30 | 31 | 32 | | | + // +----+----+----+----+ + + // | 40 | 41 | 42 | 43 | | + // +----+----+----+----+----+ + // | 50 | 51 | 52 | 53 | 54 | + // +----+----+----+----+----+ + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', { contents: '02', rowspan: 2 }, { contents: '03', rowspan: 3 }, { contents: '04', rowspan: 4 } ], + [ '20', '21' ], + [ '30', '31', '32' ], + [ '40', '41', '42', '43' ], + [ '50', '51', '52', '53', '54' ] + ] ) ); + } ); + + it( 'should decrease rowspan of table cells from previous rows (row-spanned cells on different rows)', () => { setData( model, modelTable( [ [ { rowspan: 4, contents: '00' }, { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ], [ { rowspan: 2, contents: '13' }, '14' ], - [ '22', '23', '24' ], - [ '30', '31', '32', '33', '34' ] + [ '22', '24' ], + [ '31', '32', '33', '34' ] ] ) ); tableUtils.removeRows( root.getChild( 0 ), { at: 2 } ); @@ -806,11 +963,11 @@ describe( 'TableUtils', () => { assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ], [ '13', '14' ], - [ '30', '31', '32', '33', '34' ] + [ '31', '32', '33', '34' ] ] ) ); } ); - it( 'should move rowspaned cells to row below removing it\'s row', () => { + it( 'should move row-spanned cells to a row below removing it\'s row', () => { setData( model, modelTable( [ [ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02' ], [ '12' ], @@ -826,6 +983,21 @@ describe( 'TableUtils', () => { [ '30', '31', '32' ] ] ) ); } ); + + it( 'should move row-spanned cells to a row below removing it\'s row (other cell is overlapping removed row)', () => { + setData( model, modelTable( [ + [ '00', { rowspan: 3, contents: '01' }, '02', '03', '04' ], + [ '10', { rowspan: 2, contents: '12' }, '13', '14' ], + [ '20', '23', '24' ] + ] ) ); + + tableUtils.removeRows( root.getChild( 0 ), { at: 1 } ); + + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '00', { rowspan: 2, contents: '01' }, '02', '03', '04' ], + [ '20', '12', '23', '24' ] + ] ) ); + } ); } ); describe( 'many rows', () => { @@ -924,21 +1096,85 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should properly calculate truncated rowspans', () => { + it( 'should move row-spanned cells to a row after removed rows section', () => { + setData( model, modelTable( [ + [ '00', '01', '02', '03' ], + [ { rowspan: 4, contents: '10' }, { rowspan: 3, contents: '11' }, { rowspan: 2, contents: '12' }, '13' ], + [ { rowspan: 3, contents: '23' } ], + [ '32' ], + [ '41', '42' ] + ] ) ); + + tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 2 } ); + + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '00', '01', '02', '03' ], + [ { rowspan: 2, contents: '10' }, '11', '32', { rowspan: 2, contents: '23' } ], + [ '41', '42' ] + ] ) ); + } ); + + it( 'should decrease rowspan of table cells from rows before removed rows section', () => { setData( model, modelTable( [ - [ '00', { contents: '01', rowspan: 3 } ], + [ { rowspan: 4, contents: '00' }, { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03' ], + [ '13' ], + [ '22', '23' ], + [ '31', '32', '33' ] + ] ) ); + + tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 2 } ); + + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ { rowspan: 2, contents: '00' }, '01', '02', '03' ], + [ '31', '32', '33' ] + ] ) ); + } ); + + it( 'should decrease rowspan of table cells from previous rows', () => { + // +----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | + // +----+ + + + + + // | 10 | | | | | + // +----+----+ + + + + // | 20 | 21 | | | | + // +----+----+----+ + + + // | 30 | 31 | 32 | | | + // +----+----+----+----+ + + // | 40 | 41 | 42 | 43 | | + // +----+----+----+----+----+ + // | 50 | 51 | 52 | 53 | 54 | + // +----+----+----+----+----+ + setData( model, modelTable( [ + [ '00', { contents: '01', rowspan: 2 }, { contents: '02', rowspan: 3 }, { contents: '03', rowspan: 4 }, + { contents: '04', rowspan: 5 } ], [ '10' ], - [ '20' ] + [ '20', '21' ], + [ '30', '31', '32' ], + [ '40', '41', '42', '43' ], + [ '50', '51', '52', '53', '54' ] ] ) ); - tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } ); + tableUtils.removeRows( root.getChild( 0 ), { at: 2, rows: 2 } ); + // +----+----+----+----+----+ + // | 00 | 01 | 02 | 03 | 04 | + // +----+ + + + + + // | 10 | | | | | + // +----+----+----+----+ + + // | 40 | 41 | 42 | 43 | | + // +----+----+----+----+----+ + // | 50 | 51 | 52 | 53 | 54 | + // +----+----+----+----+----+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ - [ '20', '01' ] + [ '00', { contents: '01', rowspan: 2 }, { contents: '02', rowspan: 2 }, { contents: '03', rowspan: 2 }, + { contents: '04', rowspan: 3 } ], + [ '10' ], + [ '40', '41', '42', '43' ], + [ '50', '51', '52', '53', '54' ] ] ) ); } ); - it( 'should create one undo step (1 batch)', () => { + it( 'should re-use batch to create one undo step', () => { setData( model, modelTable( [ [ '00', '01' ], [ '10', '11' ], @@ -954,7 +1190,9 @@ describe( 'TableUtils', () => { createdBatches.add( operation.batch ); } ); - tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } ); + const batch = model.createBatch(); + + tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2, batch } ); expect( createdBatches.size ).to.equal( 1 ); } ); @@ -962,6 +1200,20 @@ describe( 'TableUtils', () => { } ); describe( 'removeColumns()', () => { + beforeEach( () => { + return ModelTestEditor.create( { + plugins: [ TableUtils ] + } ).then( newEditor => { + editor = newEditor; + model = editor.model; + root = model.document.getRoot( 'main' ); + tableUtils = editor.plugins.get( TableUtils ); + + defaultSchema( model.schema ); + defaultConversion( editor.conversion ); + } ); + } ); + describe( 'single row', () => { it( 'should remove a given column', () => { setData( model, modelTable( [ @@ -1076,7 +1328,7 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should remove column if other column is rowspanned (last column)', () => { + it( 'should remove column if other column is row-spanned (last column)', () => { setData( model, modelTable( [ [ '00', { rowspan: 2, contents: '01' } ], [ '10' ] @@ -1089,7 +1341,7 @@ describe( 'TableUtils', () => { ] ) ); } ); - it( 'should remove column if other column is rowspanned (first column)', () => { + it( 'should remove column if other column is row-spanned (first column)', () => { setData( model, modelTable( [ [ { rowspan: 2, contents: '00' }, '01' ], [ '11' ]