diff --git a/packages/ckeditor5-table/src/commands/insertrowcommand.js b/packages/ckeditor5-table/src/commands/insertrowcommand.js index 623ece4da5f..cbe17719eb0 100644 --- a/packages/ckeditor5-table/src/commands/insertrowcommand.js +++ b/packages/ckeditor5-table/src/commands/insertrowcommand.js @@ -78,6 +78,6 @@ export default class InsertRowCommand extends Command { const row = insertAbove ? rowIndexes.first : rowIndexes.last; const table = findAncestor( 'table', affectedTableCells[ 0 ] ); - tableUtils.insertRows( table, { rows: 1, at: insertAbove ? row : row + 1 } ); + tableUtils.insertRows( table, { at: insertAbove ? row : row + 1, copyStructureFromAbove: !insertAbove } ); } } diff --git a/packages/ckeditor5-table/src/tableutils.js b/packages/ckeditor5-table/src/tableutils.js index a4751e7bc69..25e929162b9 100644 --- a/packages/ckeditor5-table/src/tableutils.js +++ b/packages/ckeditor5-table/src/tableutils.js @@ -116,12 +116,19 @@ export default class TableUtils extends Plugin { * @param {Object} options * @param {Number} [options.at=0] The row index at which the rows will be inserted. * @param {Number} [options.rows=1] The number of rows to insert. + * @param {Boolean|undefined} [options.copyStructureFromAbove] The flag for copying row structure. Note that + * the row structure will not be copied if this option is not provided. */ insertRows( table, options = {} ) { const model = this.editor.model; const insertAt = options.at || 0; const rowsToInsert = options.rows || 1; + const isCopyStructure = options.copyStructureFromAbove !== undefined; + const copyStructureFrom = options.copyStructureFromAbove ? insertAt - 1 : insertAt; + + const rows = this.getRows( table ); + const columns = this.getColumns( table ); model.change( writer => { const headingRows = table.getAttribute( 'headingRows' ) || 0; @@ -131,38 +138,58 @@ export default class TableUtils extends Plugin { writer.setAttribute( 'headingRows', headingRows + rowsToInsert, table ); } - // Inserting at the end and at the beginning of a table doesn't require to calculate anything special. - if ( insertAt === 0 || insertAt === table.childCount ) { - createEmptyRows( writer, table, insertAt, rowsToInsert, this.getColumns( table ) ); + // Inserting at the end or at the beginning of a table doesn't require to calculate anything special. + if ( !isCopyStructure && ( insertAt === 0 || insertAt === rows ) ) { + createEmptyRows( writer, table, insertAt, rowsToInsert, columns ); return; } - // Iterate over all rows above inserted rows in order to check for rowspanned cells. - const tableIterator = new TableWalker( table, { endRow: insertAt } ); + // Iterate over all the rows above the inserted rows in order to check for the row-spanned cells. + const walkerEndRow = isCopyStructure ? Math.max( insertAt, copyStructureFrom ) : insertAt; + const tableIterator = new TableWalker( table, { endRow: walkerEndRow } ); + + // Store spans of the reference row to reproduce it's structure. This array is column number indexed. + const rowColSpansMap = new Array( columns ).fill( 1 ); - // Will hold number of cells needed to insert in created rows. - // The number might be different then table cell width when there are rowspanned cells. - let cellsToInsert = 0; + for ( const { row, column, rowspan, colspan, cell } of tableIterator ) { + const lastCellRow = row + rowspan - 1; - for ( const { row, rowspan, colspan, cell } of tableIterator ) { - const isBeforeInsertedRow = row < insertAt; - const overlapsInsertedRow = row + rowspan > insertAt; + const isOverlappingInsertedRow = row < insertAt && insertAt <= lastCellRow; + const isReferenceRow = row <= copyStructureFrom && copyStructureFrom <= lastCellRow; - if ( isBeforeInsertedRow && overlapsInsertedRow ) { - // This cell overlaps inserted rows so we need to expand it further. + // If the cell is row-spanned and overlaps the inserted row, then reserve space for it in the row map. + if ( isOverlappingInsertedRow ) { + // This cell overlaps the inserted rows so we need to expand it further. writer.setAttribute( 'rowspan', rowspan + rowsToInsert, cell ); - } - // Calculate how many cells to insert based on the width of cells in a row at insert position. - // It might be lower then table width as some cells might overlaps inserted row. - // In the table above the cell 'a' overlaps inserted row so only two empty cells are need to be created. - if ( row === insertAt ) { - cellsToInsert += colspan; + // Mark this cell with negative number to indicate how many cells should be skipped when adding the new cells. + rowColSpansMap[ column ] = -colspan; + } + // Store the colspan from reference row. + else if ( isCopyStructure && isReferenceRow ) { + rowColSpansMap[ column ] = colspan; } } - createEmptyRows( writer, table, insertAt, rowsToInsert, cellsToInsert ); + for ( let rowIndex = 0; rowIndex < rowsToInsert; rowIndex++ ) { + const tableRow = writer.createElement( 'tableRow' ); + + writer.insert( tableRow, table, insertAt ); + + for ( let cellIndex = 0; cellIndex < rowColSpansMap.length; cellIndex++ ) { + const colspan = rowColSpansMap[ cellIndex ]; + const insertPosition = writer.createPositionAt( tableRow, 'end' ); + + // Insert the empty cell only if this slot is not row-spanned from any other cell. + if ( colspan > 0 ) { + createEmptyTableCell( writer, insertPosition, colspan > 1 ? { colspan } : null ); + } + + // Skip the col-spanned slots, there won't be any cells. + cellIndex += Math.abs( colspan ) - 1; + } + } } ); } diff --git a/packages/ckeditor5-table/tests/commands/insertrowcommand.js b/packages/ckeditor5-table/tests/commands/insertrowcommand.js index 721ed9e461f..6c88262bf62 100644 --- a/packages/ckeditor5-table/tests/commands/insertrowcommand.js +++ b/packages/ckeditor5-table/tests/commands/insertrowcommand.js @@ -232,6 +232,33 @@ describe( 'InsertRowCommand', () => { [ '', '' ] ] ) ); } ); + + it( 'should copy the row structure from the selected row', () => { + // +----+----+----+ + // | 00 | 01 | + // +----+----+----+ + // | 10 | 11 | 12 | + // +----+----+----+ + setData( model, modelTable( [ + [ '[]00', { contents: '01', colspan: 2 } ], + [ '10', '11', '12' ] + ] ) ); + + command.execute(); + + // +----+----+----+ + // | 00 | 01 | + // +----+----+----+ + // | | | + // +----+----+----+ + // | 10 | 11 | 12 | + // +----+----+----+ + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '00', { contents: '01', colspan: 2 } ], + [ '', { contents: '', colspan: 2 } ], + [ '10', '11', '12' ] + ] ) ); + } ); } ); } ); @@ -367,6 +394,33 @@ describe( 'InsertRowCommand', () => { [ 0, 0 ] ] ); } ); + + it( 'should copy the row structure from the selected row', () => { + // +----+----+----+ + // | 00 | 01 | + // +----+----+----+ + // | 10 | 11 | 12 | + // +----+----+----+ + setData( model, modelTable( [ + [ '[]00', { contents: '01', colspan: 2 } ], + [ '10', '11', '12' ] + ] ) ); + + command.execute(); + + // +----+----+----+ + // | | | + // +----+----+----+ + // | 00 | 01 | + // +----+----+----+ + // | 10 | 11 | 12 | + // +----+----+----+ + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '', { contents: '', colspan: 2 } ], + [ '00', { contents: '01', colspan: 2 } ], + [ '10', '11', '12' ] + ] ) ); + } ); } ); } ); } ); diff --git a/packages/ckeditor5-table/tests/tableutils.js b/packages/ckeditor5-table/tests/tableutils.js index 23b24d0c6d3..fedacaade06 100644 --- a/packages/ckeditor5-table/tests/tableutils.js +++ b/packages/ckeditor5-table/tests/tableutils.js @@ -219,6 +219,88 @@ describe( 'TableUtils', () => { [ '', '' ] ] ) ); } ); + + describe( 'with copyStructureFrom enabled', () => { + beforeEach( () => { + // +----+----+----+----+----+----+ + // | 00 | 01 | 03 | 04 | 05 | + // +----+ + +----+----+ + // | 10 | | | 14 | + // +----+----+----+----+----+----+ + setData( model, modelTable( [ + [ '00', { contents: '01', colspan: 2, rowspan: 2 }, { contents: '03', rowspan: 2 }, '04', '05' ], + [ '10', { contents: '14', colspan: 2 } ] + ] ) ); + } ); + + it( 'should copy structure from the first row', () => { + tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 0, rows: 1, copyStructureFromAbove: false } ); + + // +----+----+----+----+----+----+ + // | | | | | | + // +----+----+----+----+----+----+ + // | 00 | 01 | 03 | 04 | 05 | + // +----+ + +----+----+ + // | 10 | | | 14 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '', { contents: '', colspan: 2 }, '', '', '' ], + [ '00', { contents: '01', colspan: 2, rowspan: 2 }, { contents: '03', rowspan: 2 }, '04', '05' ], + [ '10', { contents: '14', colspan: 2 } ] + ] ) ); + } ); + + it( 'should copy structure from the first row and properly handle row-spanned cells', () => { + tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 1, rows: 1, copyStructureFromAbove: true } ); + + // +----+----+----+----+----+----+ + // | 00 | 01 | 03 | 04 | 05 | + // +----+ + +----+----+ + // | | | | | | + // +----+ + +----+----+ + // | 10 | | | 14 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '00', { contents: '01', colspan: 2, rowspan: 3 }, { contents: '03', rowspan: 3 }, '04', '05' ], + [ '', '', '' ], + [ '10', { contents: '14', colspan: 2 } ] + ] ) ); + } ); + + it( 'should copy structure from the last row', () => { + tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 1, copyStructureFromAbove: true } ); + + // +----+----+----+----+----+----+ + // | 00 | 01 | 03 | 04 | 05 | + // +----+ + +----+----+ + // | 10 | | | 14 | + // +----+----+----+----+----+----+ + // | | | | | + // +----+----+----+----+----+----+ + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '00', { contents: '01', colspan: 2, rowspan: 2 }, { contents: '03', rowspan: 2 }, '04', '05' ], + [ '10', { contents: '14', colspan: 2 } ], + [ '', { contents: '', colspan: 2 }, '', { contents: '', colspan: 2 } ] + ] ) ); + } ); + + it( 'should copy structure from the last row and properly handle row-spanned cells', () => { + tableUtils.insertRows( root.getNodeByPath( [ 0 ] ), { at: 1, rows: 1, copyStructureFromAbove: false } ); + + // +----+----+----+----+----+----+ + // | 00 | 01 | 03 | 04 | 05 | + // +----+ + +----+----+ + // | | | | | + // +----+ + +----+----+ + // | 10 | | | 14 | + // +----+----+----+----+----+----+ + assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [ + [ '00', { contents: '01', colspan: 2, rowspan: 3 }, { contents: '03', rowspan: 3 }, '04', '05' ], + [ '', { contents: '', colspan: 2 } ], + [ '10', { contents: '14', colspan: 2 } ] + ] ) ); + } ); + } ); } ); describe( 'insertColumns()', () => {