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

Commit

Permalink
Merge pull request #297 from ckeditor/i/6545
Browse files Browse the repository at this point in the history
Feature: Introduce the `TableUtils.removeRows()` method. Closes ckeditor/ckeditor5#6545.
  • Loading branch information
oleq authored Apr 7, 2020
2 parents fd1d5da + fe3c1df commit c6770ba
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 78 deletions.
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 rows from the given `table`.
*
* This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
* and table headings values.
*
* editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
*
* Executing the above code in the context of the table on the left will transform its structure as presented on the right:
*
* row index
* ┌───┬───┬───┐ `at` = 1 ┌───┬───┬───┐
* 0 │ a │ b │ c │ `rows` = 2 │ a │ b │ c │ 0
* │ ├───┼───┤ │ ├───┼───┤
* 1 │ │ d │ e │ <-- remove from here │ │ h │ i │ 1
* │ ├───┼───┤ will give: ├───┼───┼───┤
* 2 │ │ f │ g │ │ j │ k │ l │ 2
* │ ├───┼───┤ └───┴───┴───┘
* 3 │ │ h │ i │
* ├───┼───┼───┤
* 4 │ j │ k │ l │
* └───┴───┴───┘
*
* @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 ) {
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 );
}

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;
}
4 changes: 2 additions & 2 deletions tests/commands/removerowcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,15 +464,15 @@ describe( 'RemoveRowCommand', () => {
setData( model, modelTable( [
[ { rowspan: 3, contents: '[]00' }, { rowspan: 2, contents: '01' }, '02' ],
[ '12' ],
[ '22' ],
[ '21', '22' ],
[ '30', '31', '32' ]
] ) );

command.execute();

assertEqualMarkup( getData( model ), modelTable( [
[ { rowspan: 2, contents: '[]00' }, '01', '12' ],
[ '22' ],
[ '21', '22' ],
[ '30', '31', '32' ]
] ) );
} );
Expand Down
Loading

0 comments on commit c6770ba

Please sign in to comment.