Skip to content

Commit

Permalink
Merge pull request #6748 from ckeditor/i/6549
Browse files Browse the repository at this point in the history
Other (table): Adding a new row in the table copies the structure of the selected row. Closes #6549.
  • Loading branch information
jodator authored May 15, 2020
2 parents af9ed53 + 7b84d7d commit 9f20911
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 21 deletions.
2 changes: 1 addition & 1 deletion packages/ckeditor5-table/src/commands/insertrowcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } );
}
}
67 changes: 47 additions & 20 deletions packages/ckeditor5-table/src/tableutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
} );
}

Expand Down
54 changes: 54 additions & 0 deletions packages/ckeditor5-table/tests/commands/insertrowcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ]
] ) );
} );
} );
} );

Expand Down Expand Up @@ -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' ]
] ) );
} );
} );
} );
} );
82 changes: 82 additions & 0 deletions packages/ckeditor5-table/tests/tableutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down

0 comments on commit 9f20911

Please sign in to comment.