@@ -267,13 +267,13 @@ export default class TableUtils extends Plugin {
267
267
* ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
268
268
* 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
269
269
* │ ├───┼───┤ │ ├───┼───┤
270
- * 1 │ │ d │ e │ <-- remove from here │ │ h │ i │ 1
271
- * │ ├───┼ ───┤ will give: ├───┼───┼───┤
272
- * 2 │ │ f │ g │ │ j │ k │ l │ 2
273
- * │ ├───┼ ───┤ └───┴───┴───┘
274
- * 3 │ │ h │ i │
270
+ * 1 │ │ d │ e │ <-- remove from here │ │ d │ g │ 1
271
+ * │ │ ├ ───┤ will give: ├───┼───┼───┤
272
+ * 2 │ │ │ f │ │ h │ i │ j │ 2
273
+ * │ │ ├ ───┤ └───┴───┴───┘
274
+ * 3 │ │ │ g │
275
275
* ├───┼───┼───┤
276
- * 4 │ j │ k │ l │
276
+ * 4 │ h │ i │ j │
277
277
* └───┴───┴───┘
278
278
*
279
279
* @param {module:engine/model/element~Element } table
@@ -283,27 +283,35 @@ export default class TableUtils extends Plugin {
283
283
*/
284
284
removeRows ( table , options ) {
285
285
const model = this . editor . model ;
286
- const first = options . at ;
287
- const rowsToRemove = options . rows || 1 ;
288
286
287
+ const rowsToRemove = options . rows || 1 ;
288
+ const first = options . at ;
289
289
const last = first + rowsToRemove - 1 ;
290
290
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
291
297
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.
292
304
for ( let i = last ; i >= first ; i -- ) {
293
- removeRow ( table , i , writer ) ;
305
+ writer . remove ( table . getChild ( i ) ) ;
294
306
}
295
307
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 ) ;
306
311
}
312
+
313
+ // 2d. Adjust heading rows if removed rows were in a heading section.
314
+ updateHeadingRows ( table , first , last , model , writer . batch ) ;
307
315
} ) ;
308
316
}
309
317
@@ -730,60 +738,123 @@ function breakSpanEvenly( span, numberOfCells ) {
730
738
return { newCellsSpan, updatedSpan } ;
731
739
}
732
740
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 ;
752
744
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 ;
766
748
767
- writer . remove ( tableRow ) ;
749
+ writer . setAttribute ( 'headingColumns' , headingColumns - headingsRemoved , table ) ;
750
+ }
768
751
}
769
752
770
753
// 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
+ } ) ;
774
765
}
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
+ }
775
807
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 } ;
777
829
}
778
830
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
+ } ) ;
782
837
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 ) ;
786
840
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
+ }
788
859
}
789
860
}
0 commit comments