Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit c8d8d32

Browse files
authored
Merge pull request #303 from ckeditor/i/6502
Fix: Removing rows in complex tables should properly move cells from removed rows. Closes ckeditor/ckeditor5#6502.
2 parents 7213f93 + b04996d commit c8d8d32

File tree

2 files changed

+415
-94
lines changed

2 files changed

+415
-94
lines changed

src/tableutils.js

+134-63
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,13 @@ export default class TableUtils extends Plugin {
267267
* ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
268268
* 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
269269
* │ ├───┼───┤ │ ├───┼───┤
270-
* 1 │ │ d │ e │ <-- remove from here │ │ hi │ 1
271-
* │ ├───┼───┤ will give: ├───┼───┼───┤
272-
* 2 │ │ f │ g │ │ jkl │ 2
273-
* │ ├───┼───┤ └───┴───┴───┘
274-
* 3 │ │ h │ i
270+
* 1 │ │ d │ e │ <-- remove from here │ │ dg │ 1
271+
* │ │ ├───┤ will give: ├───┼───┼───┤
272+
* 2 │ │ │ f │ │ hij │ 2
273+
* │ │ ├───┤ └───┴───┴───┘
274+
* 3 │ │ │ g
275275
* ├───┼───┼───┤
276-
* 4 │ jkl
276+
* 4 │ hij
277277
* └───┴───┴───┘
278278
*
279279
* @param {module:engine/model/element~Element} table
@@ -283,27 +283,35 @@ export default class TableUtils extends Plugin {
283283
*/
284284
removeRows( table, options ) {
285285
const model = this.editor.model;
286-
const first = options.at;
287-
const rowsToRemove = options.rows || 1;
288286

287+
const rowsToRemove = options.rows || 1;
288+
const first = options.at;
289289
const last = first + rowsToRemove - 1;
290290

291+
// Removing rows from table requires most calculations to be done prior to changing table structure.
292+
293+
// 1. Preparation - get row-spanned cells that have to be modified after removing rows.
294+
const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last );
295+
296+
// 2. Execution
291297
model.change( writer => {
298+
// 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
299+
// This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
300+
const rowAfterRemovedSection = last + 1;
301+
moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer );
302+
303+
// 2b. Remove all required rows.
292304
for ( let i = last; i >= first; i-- ) {
293-
removeRow( table, i, writer );
305+
writer.remove( table.getChild( i ) );
294306
}
295307

296-
const headingRows = table.getAttribute( 'headingRows' ) || 0;
297-
298-
if ( headingRows && first < headingRows ) {
299-
const newRows = getNewHeadingRowsValue( first, last, headingRows );
300-
301-
// Must be done after the changes in table structure (removing rows).
302-
// Otherwise the downcast converter for headingRows attribute will fail. ckeditor/ckeditor5#6391.
303-
model.enqueueChange( writer.batch, writer => {
304-
updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
305-
} );
308+
// 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells.
309+
for ( const { rowspan, cell } of cellsToTrim ) {
310+
updateNumericAttribute( 'rowspan', rowspan, cell, writer );
306311
}
312+
313+
// 2d. Adjust heading rows if removed rows were in a heading section.
314+
updateHeadingRows( table, first, last, model, writer.batch );
307315
} );
308316
}
309317

@@ -730,60 +738,123 @@ function breakSpanEvenly( span, numberOfCells ) {
730738
return { newCellsSpan, updatedSpan };
731739
}
732740

733-
function removeRow( table, rowIndex, writer ) {
734-
const cellsToMove = new Map();
735-
const tableRow = table.getChild( rowIndex );
736-
const tableMap = [ ...new TableWalker( table, { endRow: rowIndex } ) ];
737-
738-
// Get cells from removed row that are spanned over multiple rows.
739-
tableMap
740-
.filter( ( { row, rowspan } ) => row === rowIndex && rowspan > 1 )
741-
.forEach( ( { column, cell, rowspan } ) => cellsToMove.set( column, { cell, rowspanToSet: rowspan - 1 } ) );
742-
743-
// Reduce rowspan on cells that are above removed row and overlaps removed row.
744-
tableMap
745-
.filter( ( { row, rowspan } ) => row <= rowIndex - 1 && row + rowspan > rowIndex )
746-
.forEach( ( { cell, rowspan } ) => updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer ) );
747-
748-
// Move cells to another row.
749-
const targetRow = rowIndex + 1;
750-
const tableWalker = new TableWalker( table, { includeSpanned: true, startRow: targetRow, endRow: targetRow } );
751-
let previousCell;
741+
// Updates heading columns attribute if removing a row from head section.
742+
function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
743+
const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
752744

753-
for ( const { row, column, cell } of [ ...tableWalker ] ) {
754-
if ( cellsToMove.has( column ) ) {
755-
const { cell: cellToMove, rowspanToSet } = cellsToMove.get( column );
756-
const targetPosition = previousCell ?
757-
writer.createPositionAfter( previousCell ) :
758-
writer.createPositionAt( table.getChild( row ), 0 );
759-
writer.move( writer.createRangeOn( cellToMove ), targetPosition );
760-
updateNumericAttribute( 'rowspan', rowspanToSet, cellToMove, writer );
761-
previousCell = cellToMove;
762-
} else {
763-
previousCell = cell;
764-
}
765-
}
745+
if ( headingColumns && removedColumnIndexes.first < headingColumns ) {
746+
const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
747+
removedColumnIndexes.first + 1;
766748

767-
writer.remove( tableRow );
749+
writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
750+
}
768751
}
769752

770753
// Calculates a new heading rows value for removing rows from heading section.
771-
function getNewHeadingRowsValue( first, last, headingRows ) {
772-
if ( last < headingRows ) {
773-
return headingRows - ( last - first + 1 );
754+
function updateHeadingRows( table, first, last, model, batch ) {
755+
const headingRows = table.getAttribute( 'headingRows' ) || 0;
756+
757+
if ( first < headingRows ) {
758+
const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
759+
760+
// Must be done after the changes in table structure (removing rows).
761+
// Otherwise the downcast converter for headingRows attribute will fail. ckeditor/ckeditor5#6391.
762+
model.enqueueChange( batch, writer => {
763+
updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
764+
} );
774765
}
766+
}
767+
768+
// Finds cells that will be:
769+
// - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
770+
// - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
771+
//
772+
// Sample table with overlapping & sticking out cells:
773+
//
774+
// +----+----+----+----+----+
775+
// | 00 | 01 | 02 | 03 | 04 |
776+
// +----+ + + + +
777+
// | 10 | | | | |
778+
// +----+----+ + + +
779+
// | 20 | 21 | | | | <-- removed row
780+
// + + +----+ + +
781+
// | | | 32 | | | <-- removed row
782+
// +----+ + +----+ +
783+
// | 40 | | | 43 | |
784+
// +----+----+----+----+----+
785+
//
786+
// In a table above:
787+
// - cells to trim: '02', '03' & '04'.
788+
// - cells to move: '21' & '32'.
789+
function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
790+
const cellsToMove = new Map();
791+
const cellsToTrim = [];
792+
793+
for ( const { row, column, rowspan, cell } of new TableWalker( table, { endRow: last } ) ) {
794+
const lastRowOfCell = row + rowspan - 1;
795+
796+
const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;
797+
798+
if ( isCellStickingOutFromRemovedRows ) {
799+
const rowspanInRemovedSection = last - row + 1;
800+
const rowSpanToSet = rowspan - rowspanInRemovedSection;
801+
802+
cellsToMove.set( column, {
803+
cell,
804+
rowspan: rowSpanToSet
805+
} );
806+
}
775807

776-
return first;
808+
const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first;
809+
810+
if ( isCellOverlappingRemovedRows ) {
811+
let rowspanAdjustment;
812+
813+
// Cell fully covers removed section - trim it by removed rows count.
814+
if ( lastRowOfCell >= last ) {
815+
rowspanAdjustment = last - first + 1;
816+
}
817+
// Cell partially overlaps removed section - calculate cell's span that is in removed section.
818+
else {
819+
rowspanAdjustment = lastRowOfCell - first + 1;
820+
}
821+
822+
cellsToTrim.push( {
823+
cell,
824+
rowspan: rowspan - rowspanAdjustment
825+
} );
826+
}
827+
}
828+
return { cellsToMove, cellsToTrim };
777829
}
778830

779-
// Updates heading columns attribute if removing a row from head section.
780-
function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
781-
const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
831+
function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
832+
const tableWalker = new TableWalker( table, {
833+
includeSpanned: true,
834+
startRow: targetRowIndex,
835+
endRow: targetRowIndex
836+
} );
782837

783-
if ( headingColumns && removedColumnIndexes.first < headingColumns ) {
784-
const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
785-
removedColumnIndexes.first + 1;
838+
const tableRowMap = [ ...tableWalker ];
839+
const row = table.getChild( targetRowIndex );
786840

787-
writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
841+
let previousCell;
842+
843+
for ( const { column, cell, isSpanned } of tableRowMap ) {
844+
if ( cellsToMove.has( column ) ) {
845+
const { cell: cellToMove, rowspan } = cellsToMove.get( column );
846+
847+
const targetPosition = previousCell ?
848+
writer.createPositionAfter( previousCell ) :
849+
writer.createPositionAt( row, 0 );
850+
851+
writer.move( writer.createRangeOn( cellToMove ), targetPosition );
852+
updateNumericAttribute( 'rowspan', rowspan, cellToMove, writer );
853+
854+
previousCell = cellToMove;
855+
} else if ( !isSpanned ) {
856+
// If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
857+
previousCell = cell;
858+
}
788859
}
789860
}

0 commit comments

Comments
 (0)