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

I/6545: Introduce TableUtils.removeRows() utility method. #297

Merged
merged 9 commits into from
Apr 7, 2020
83 changes: 7 additions & 76 deletions src/commands/removerowcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@

import Command from '@ckeditor/ckeditor5-core/src/command';

import TableWalker from '../tablewalker';
import { findAncestor, updateNumericAttribute } from './utils';
import { findAncestor } from './utils';
import { getRowIndexes, getSelectionAffectedTableCells } from '../utils';

/**
Expand Down Expand Up @@ -65,77 +64,18 @@ export default class RemoveRowCommand extends Command {
// This prevents the "model-selection-range-intersects" error, caused by removing row selected cells.
writer.setSelection( writer.createSelection( table, 'on' ) );

let cellToFocus;
const rowsToRemove = removedRowIndexes.last - removedRowIndexes.first + 1;

for ( let i = removedRowIndexes.last; i >= removedRowIndexes.first; i-- ) {
const removedRowIndex = i;
this._removeRow( removedRowIndex, table, writer );
this.editor.plugins.get( 'TableUtils' ).removeRows( table, {
at: removedRowIndexes.first,
rows: rowsToRemove
} );

cellToFocus = getCellToFocus( table, removedRowIndex, columnIndexToFocus );
}

const model = this.editor.model;
const headingRows = table.getAttribute( 'headingRows' ) || 0;

if ( headingRows && removedRowIndexes.first < headingRows ) {
const newRows = getNewHeadingRowsValue( removedRowIndexes, headingRows );

// 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 );
} );
}
const cellToFocus = getCellToFocus( table, removedRowIndexes.first, columnIndexToFocus );

writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
} );
}

/**
* Removes a row from the given `table`.
*
* @private
* @param {Number} removedRowIndex Index of the row that should be removed.
* @param {module:engine/model/element~Element} table
* @param {module:engine/model/writer~Writer} writer
*/
_removeRow( removedRowIndex, table, writer ) {
const cellsToMove = new Map();
const tableRow = table.getChild( removedRowIndex );
const tableMap = [ ...new TableWalker( table, { endRow: removedRowIndex } ) ];

// Get cells from removed row that are spanned over multiple rows.
tableMap
.filter( ( { row, rowspan } ) => row === removedRowIndex && 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 <= removedRowIndex - 1 && row + rowspan > removedRowIndex )
.forEach( ( { cell, rowspan } ) => updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer ) );

// Move cells to another row.
const targetRow = removedRowIndex + 1;
const tableWalker = new TableWalker( table, { includeSpanned: true, startRow: targetRow, endRow: targetRow } );
let previousCell;

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;
}
}

writer.remove( tableRow );
}
}

// Returns a cell that should be focused before removing the row, belonging to the same column as the currently focused cell.
Expand All @@ -159,12 +99,3 @@ function getCellToFocus( table, removedRowIndex, columnToFocus ) {

return cellToFocus;
}

// Calculates a new heading rows value for removing rows from heading section.
function getNewHeadingRowsValue( removedRowIndexes, headingRows ) {
if ( removedRowIndexes.last < headingRows ) {
return headingRows - ( ( removedRowIndexes.last - removedRowIndexes.first ) + 1 );
}

return removedRowIndexes.first;
}
100 changes: 100 additions & 0 deletions src/tableutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,60 @@ export default class TableUtils extends Plugin {
} );
}

/**
* Removes a row from the given `table`.
jodator marked this conversation as resolved.
Show resolved Hide resolved
*
* This method properly re-calculate table geometry including `rowspan` attribute of any table cell that is overlapping removed row
jodator marked this conversation as resolved.
Show resolved Hide resolved
* and table headings values.
*
* editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
*
* Assuming the table on the left, the above code will transform it to the table on the right:
jodator marked this conversation as resolved.
Show resolved Hide resolved
*
* row index
* ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
* 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
* │ ├───┼───┤ │ ├───┼───┤
* 1 │ │ d │ e │ <-- remove from here │ │ i │ j │ 1
* │ ├───┼───┤ will give: ├───┼───┼───┤
* 2 │ │ g │ h │ │ k │ l │ m │ 2
* │ ├───┼───┤ └───┴───┴───┘
* 3 │ │ i │ j │
* ├───┼───┼───┤
* 4 │ k │ l │ m │
* └───┴───┴───┘
*
* @param {module:engine/model/element~Element} table
* @param {Object} options
* @param {Number} options.at The row index at which the removing rows will start.
* @param {Number} [options.rows=1] The number of rows to remove.
*/
removeRows( table, options ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one needs review. Rest of the PR is moving stuff around from RemoveRowComand - it's test are untouched so the whole logic works. New tests are for removing rows only.

const model = this.editor.model;
const first = options.at;
const rowsToRemove = options.rows || 1;

const last = first + rowsToRemove - 1;

model.change( writer => {
for ( let i = last; i >= first; i-- ) {
removeRow( table, i, writer );
jodator marked this conversation as resolved.
Show resolved Hide resolved
}

const headingRows = table.getAttribute( 'headingRows' ) || 0;

if ( headingRows && first < headingRows ) {
const newRows = getNewHeadingRowsValue( first, last, headingRows );

// 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 );
} );
}
} );
}

/**
* Divides a table cell vertically into several ones.
*
Expand Down Expand Up @@ -615,3 +669,49 @@ 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;

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;
}
}

writer.remove( tableRow );
}

// 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 );
}

return first;
}
Loading