diff --git a/src/commands/removecolumncommand.js b/src/commands/removecolumncommand.js
index e398f125..54d7d630 100644
--- a/src/commands/removecolumncommand.js
+++ b/src/commands/removecolumncommand.js
@@ -10,7 +10,8 @@
import Command from '@ckeditor/ckeditor5-core/src/command';
import TableWalker from '../tablewalker';
-import { getSelectionAffectedTableCells } from '../utils';
+import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils';
+import { findAncestor } from './utils';
/**
* The remove column command.
@@ -32,15 +33,12 @@ export default class RemoveColumnCommand extends Command {
const firstCell = selectedCells[ 0 ];
if ( firstCell ) {
- const table = firstCell.parent.parent;
+ const table = findAncestor( 'table', firstCell );
const tableColumnCount = this.editor.plugins.get( 'TableUtils' ).getColumns( table );
- const tableMap = [ ...new TableWalker( table ) ];
- const columnIndexes = tableMap.filter( entry => selectedCells.includes( entry.cell ) ).map( el => el.column ).sort();
- const minColumnIndex = columnIndexes[ 0 ];
- const maxColumnIndex = columnIndexes[ columnIndexes.length - 1 ];
+ const { first, last } = getColumnIndexes( selectedCells );
- this.isEnabled = maxColumnIndex - minColumnIndex < ( tableColumnCount - 1 );
+ this.isEnabled = last - first < ( tableColumnCount - 1 );
} else {
this.isEnabled = false;
}
diff --git a/src/commands/removerowcommand.js b/src/commands/removerowcommand.js
index 641d5b25..e4737791 100644
--- a/src/commands/removerowcommand.js
+++ b/src/commands/removerowcommand.js
@@ -60,7 +60,10 @@ export default class RemoveRowCommand extends Command {
const columnIndexToFocus = this.editor.plugins.get( 'TableUtils' ).getCellLocation( firstCell ).column;
- model.change( writer => {
+ // Use single batch to modify table in steps but in one undo step.
+ const batch = model.createBatch();
+
+ model.enqueueChange( batch, writer => {
// This prevents the "model-selection-range-intersects" error, caused by removing row selected cells.
writer.setSelection( writer.createSelection( table, 'on' ) );
@@ -68,9 +71,12 @@ export default class RemoveRowCommand extends Command {
this.editor.plugins.get( 'TableUtils' ).removeRows( table, {
at: removedRowIndexes.first,
- rows: rowsToRemove
+ rows: rowsToRemove,
+ batch
} );
+ } );
+ model.enqueueChange( batch, writer => {
const cellToFocus = getCellToFocus( table, removedRowIndexes.first, columnIndexToFocus );
writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
diff --git a/src/commands/setheadercolumncommand.js b/src/commands/setheadercolumncommand.js
index 223c6079..69c9d125 100644
--- a/src/commands/setheadercolumncommand.js
+++ b/src/commands/setheadercolumncommand.js
@@ -9,11 +9,8 @@
import Command from '@ckeditor/ckeditor5-core/src/command';
-import {
- updateNumericAttribute,
- isHeadingColumnCell
-} from './utils';
-import { getSelectionAffectedTableCells } from '../utils';
+import { findAncestor, isHeadingColumnCell, updateNumericAttribute } from './utils';
+import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils';
/**
* The header column command.
@@ -66,26 +63,19 @@ export default class SetHeaderColumnCommand extends Command {
* the `forceValue` parameter instead of the current model state.
*/
execute( options = {} ) {
- const model = this.editor.model;
- const tableUtils = this.editor.plugins.get( 'TableUtils' );
-
- const selectedCells = getSelectionAffectedTableCells( model.document.selection );
- const firstCell = selectedCells[ 0 ];
- const lastCell = selectedCells[ selectedCells.length - 1 ];
- const tableRow = firstCell.parent;
- const table = tableRow.parent;
-
- const [ selectedColumnMin, selectedColumnMax ] =
- // Returned cells might not necessary be in order, so make sure to sort it.
- [ tableUtils.getCellLocation( firstCell ).column, tableUtils.getCellLocation( lastCell ).column ].sort();
-
if ( options.forceValue === this.value ) {
return;
}
- const headingColumnsToSet = this.value ? selectedColumnMin : selectedColumnMax + 1;
+ const model = this.editor.model;
+ const selectedCells = getSelectionAffectedTableCells( model.document.selection );
+ const { first, last } = getColumnIndexes( selectedCells );
+
+ const headingColumnsToSet = this.value ? first : last + 1;
model.change( writer => {
+ const table = findAncestor( 'table', selectedCells[ 0 ] );
+
updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );
} );
}
diff --git a/src/commands/setheaderrowcommand.js b/src/commands/setheaderrowcommand.js
index c318c887..a4fc9680 100644
--- a/src/commands/setheaderrowcommand.js
+++ b/src/commands/setheaderrowcommand.js
@@ -9,8 +9,8 @@
import Command from '@ckeditor/ckeditor5-core/src/command';
-import { createEmptyTableCell, updateNumericAttribute } from './utils';
-import { getSelectionAffectedTableCells } from '../utils';
+import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './utils';
+import { getRowIndexes, getSelectionAffectedTableCells } from '../utils';
import TableWalker from '../tablewalker';
/**
@@ -62,24 +62,16 @@ export default class SetHeaderRowCommand extends Command {
* the `forceValue` parameter instead of the current model state.
*/
execute( options = {} ) {
- const model = this.editor.model;
-
- const selectedCells = getSelectionAffectedTableCells( model.document.selection );
- const firstCell = selectedCells[ 0 ];
- const lastCell = selectedCells[ selectedCells.length - 1 ];
- const table = firstCell.parent.parent;
-
- const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
-
- const [ selectedRowMin, selectedRowMax ] =
- // Returned cells might not necessary be in order, so make sure to sort it.
- [ firstCell.parent.index, lastCell.parent.index ].sort();
-
if ( options.forceValue === this.value ) {
return;
}
+ const model = this.editor.model;
+ const selectedCells = getSelectionAffectedTableCells( model.document.selection );
+ const table = findAncestor( 'table', selectedCells[ 0 ] );
- const headingRowsToSet = this.value ? selectedRowMin : selectedRowMax + 1;
+ const { first, last } = getRowIndexes( selectedCells );
+ const headingRowsToSet = this.value ? first : last + 1;
+ const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
model.change( writer => {
if ( headingRowsToSet ) {
diff --git a/src/converters/downcast.js b/src/converters/downcast.js
index a32f8998..cbd9ad0a 100644
--- a/src/converters/downcast.js
+++ b/src/converters/downcast.js
@@ -209,9 +209,6 @@ export function downcastTableHeadingRowsChange( options = {} ) {
renameViewTableCell( tableCell, 'th', conversionApi, asWidget );
}
}
-
- // Cleanup: this will remove any empty section from the view which may happen when moving all rows from a table section.
- removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi );
}
// The head section has shrunk so move rows from to
.
else {
@@ -234,11 +231,12 @@ export function downcastTableHeadingRowsChange( options = {} ) {
for ( const tableWalkerValue of tableWalker ) {
renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conversionApi, asWidget );
}
-
- // Cleanup: this will remove any empty section from the view which may happen when moving all rows from a table section.
- removeTableSectionIfEmpty( 'thead', viewTable, conversionApi );
}
+ // Cleanup: Ensure that thead & tbody sections are removed if left empty after moving rows. See #6437, #6391.
+ removeTableSectionIfEmpty( 'thead', viewTable, conversionApi );
+ removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi );
+
function isBetween( index, lower, upper ) {
return index > lower && index < upper;
}
@@ -298,6 +296,7 @@ export function downcastRemoveRow() {
const viewStart = mapper.toViewPosition( data.position ).getLastMatchingPosition( value => !value.item.is( 'tr' ) );
const viewItem = viewStart.nodeAfter;
const tableSection = viewItem.parent;
+ const viewTable = tableSection.parent;
// Remove associated from the view.
const removeRange = viewWriter.createRangeOn( viewItem );
@@ -307,11 +306,9 @@ export function downcastRemoveRow() {
mapper.unbindViewElement( child );
}
- // Check if table section has any children left - if not remove it from the view.
- if ( !tableSection.childCount ) {
- // No need to unbind anything as table section is not represented in the model.
- viewWriter.remove( viewWriter.createRangeOn( tableSection ) );
- }
+ // Cleanup: Ensure that thead & tbody sections are removed if left empty after removing rows. See #6437, #6391.
+ removeTableSectionIfEmpty( 'thead', viewTable, conversionApi );
+ removeTableSectionIfEmpty( 'tbody', viewTable, conversionApi );
}, { priority: 'higher' } );
}
diff --git a/src/tableutils.js b/src/tableutils.js
index a1e1b17b..91f50ee7 100644
--- a/src/tableutils.js
+++ b/src/tableutils.js
@@ -267,13 +267,13 @@ export default class TableUtils extends Plugin {
* ┌───┬───┬───┐ `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 │
+ * 1 │ │ d │ e │ <-- remove from here │ │ d │ g │ 1
+ * │ │ ├───┤ will give: ├───┼───┼───┤
+ * 2 │ │ │ f │ │ h │ i │ j │ 2
+ * │ │ ├───┤ └───┴───┴───┘
+ * 3 │ │ │ g │
* ├───┼───┼───┤
- * 4 │ j │ k │ l │
+ * 4 │ h │ i │ j │
* └───┴───┴───┘
*
* @param {module:engine/model/element~Element} table
@@ -283,27 +283,36 @@ export default class TableUtils extends Plugin {
*/
removeRows( table, options ) {
const model = this.editor.model;
- const first = options.at;
- const rowsToRemove = options.rows || 1;
+ const rowsToRemove = options.rows || 1;
+ const first = options.at;
const last = first + rowsToRemove - 1;
+ const batch = options.batch || 'default';
- model.change( writer => {
- for ( let i = last; i >= first; i-- ) {
- removeRow( table, i, writer );
- }
+ // Removing rows from table requires most calculations to be done prior to changing table structure.
- const headingRows = table.getAttribute( 'headingRows' ) || 0;
+ // 1. Preparation - get row-spanned cells that have to be modified after removing rows.
+ const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last );
- if ( headingRows && first < headingRows ) {
- const newRows = getNewHeadingRowsValue( first, last, headingRows );
+ // 2. Execution
+ model.enqueueChange( batch, writer => {
+ // 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
+ // This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
+ const rowAfterRemovedSection = last + 1;
+ moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer );
- // 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 );
- } );
+ // 2b. Remove all required rows.
+ for ( let i = last; i >= first; i-- ) {
+ writer.remove( table.getChild( i ) );
+ }
+
+ // 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells.
+ for ( const { rowspan, cell } of cellsToTrim ) {
+ updateNumericAttribute( 'rowspan', rowspan, cell, writer );
}
+
+ // 2d. Adjust heading rows if removed rows were in a heading section.
+ updateHeadingRows( table, first, last, model, batch );
} );
}
@@ -730,60 +739,123 @@ 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;
+// Updates heading columns attribute if removing a row from head section.
+function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
+ const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
- 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;
- }
- }
+ if ( headingColumns && removedColumnIndexes.first < headingColumns ) {
+ const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
+ removedColumnIndexes.first + 1;
- writer.remove( tableRow );
+ writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
+ }
}
// 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 );
+function updateHeadingRows( table, first, last, model, batch ) {
+ const headingRows = table.getAttribute( 'headingRows' ) || 0;
+
+ if ( first < headingRows ) {
+ const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;
+
+ // 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( batch, writer => {
+ updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
+ } );
}
+}
- return first;
+// Finds cells that will be:
+// - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
+// - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
+//
+// Sample table with overlapping & sticking out cells:
+//
+// +----+----+----+----+----+
+// | 00 | 01 | 02 | 03 | 04 |
+// +----+ + + + +
+// | 10 | | | | |
+// +----+----+ + + +
+// | 20 | 21 | | | | <-- removed row
+// + + +----+ + +
+// | | | 32 | | | <-- removed row
+// +----+ + +----+ +
+// | 40 | | | 43 | |
+// +----+----+----+----+----+
+//
+// In a table above:
+// - cells to trim: '02', '03' & '04'.
+// - cells to move: '21' & '32'.
+function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
+ const cellsToMove = new Map();
+ const cellsToTrim = [];
+
+ for ( const { row, column, rowspan, cell } of new TableWalker( table, { endRow: last } ) ) {
+ const lastRowOfCell = row + rowspan - 1;
+
+ const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;
+
+ if ( isCellStickingOutFromRemovedRows ) {
+ const rowspanInRemovedSection = last - row + 1;
+ const rowSpanToSet = rowspan - rowspanInRemovedSection;
+
+ cellsToMove.set( column, {
+ cell,
+ rowspan: rowSpanToSet
+ } );
+ }
+
+ const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first;
+
+ if ( isCellOverlappingRemovedRows ) {
+ let rowspanAdjustment;
+
+ // Cell fully covers removed section - trim it by removed rows count.
+ if ( lastRowOfCell >= last ) {
+ rowspanAdjustment = last - first + 1;
+ }
+ // Cell partially overlaps removed section - calculate cell's span that is in removed section.
+ else {
+ rowspanAdjustment = lastRowOfCell - first + 1;
+ }
+
+ cellsToTrim.push( {
+ cell,
+ rowspan: rowspan - rowspanAdjustment
+ } );
+ }
+ }
+ return { cellsToMove, cellsToTrim };
}
-// Updates heading columns attribute if removing a row from head section.
-function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
- const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
+function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
+ const tableWalker = new TableWalker( table, {
+ includeSpanned: true,
+ startRow: targetRowIndex,
+ endRow: targetRowIndex
+ } );
- if ( headingColumns && removedColumnIndexes.first < headingColumns ) {
- const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
- removedColumnIndexes.first + 1;
+ const tableRowMap = [ ...tableWalker ];
+ const row = table.getChild( targetRowIndex );
- writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
+ let previousCell;
+
+ for ( const { column, cell, isSpanned } of tableRowMap ) {
+ if ( cellsToMove.has( column ) ) {
+ const { cell: cellToMove, rowspan } = cellsToMove.get( column );
+
+ const targetPosition = previousCell ?
+ writer.createPositionAfter( previousCell ) :
+ writer.createPositionAt( row, 0 );
+
+ writer.move( writer.createRangeOn( cellToMove ), targetPosition );
+ updateNumericAttribute( 'rowspan', rowspan, cellToMove, writer );
+
+ previousCell = cellToMove;
+ } else if ( !isSpanned ) {
+ // If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
+ previousCell = cell;
+ }
}
}
diff --git a/src/utils.js b/src/utils.js
index 76317fdb..f5536c11 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -9,6 +9,7 @@
import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import { findAncestor } from './commands/utils';
+import TableWalker from './tablewalker';
/**
* Converts a given {@link module:engine/view/element~Element} to a table widget:
@@ -138,24 +139,54 @@ export function getSelectionAffectedTableCells( selection ) {
}
/**
- * Returns a helper object with `first` and `last` row index contained in given `tableCells`.
+ * Returns an object with `first` and `last` row index contained in the given `tableCells`.
*
* const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
*
* const { first, last } = getRowIndexes( selectedTableCells );
*
- * console.log( `Selected rows ${ first } to ${ last }` );
+ * console.log( `Selected rows: ${ first } to ${ last }` );
*
- * @package {Array.}
+ * @param {Array.} tableCells
* @returns {Object} Returns an object with `first` and `last` table row indexes.
*/
export function getRowIndexes( tableCells ) {
- const allIndexesSorted = tableCells.map( cell => cell.parent.index ).sort();
+ const indexes = tableCells.map( cell => cell.parent.index );
- return {
- first: allIndexesSorted[ 0 ],
- last: allIndexesSorted[ allIndexesSorted.length - 1 ]
- };
+ return getFirstLastIndexesObject( indexes );
+}
+
+/**
+ * Returns an object with `first` and `last` column index contained in the given `tableCells`.
+ *
+ * const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
+ *
+ * const { first, last } = getColumnIndexes( selectedTableCells );
+ *
+ * console.log( `Selected columns: ${ first } to ${ last }` );
+ *
+ * @param {Array.} tableCells
+ * @returns {Object} Returns an object with `first` and `last` table column indexes.
+ */
+export function getColumnIndexes( tableCells ) {
+ const table = findAncestor( 'table', tableCells[ 0 ] );
+ const tableMap = [ ...new TableWalker( table ) ];
+
+ const indexes = tableMap
+ .filter( entry => tableCells.includes( entry.cell ) )
+ .map( entry => entry.column );
+
+ return getFirstLastIndexesObject( indexes );
+}
+
+// Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
+function getFirstLastIndexesObject( indexes ) {
+ const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );
+
+ const first = allIndexesSorted[ 0 ];
+ const last = allIndexesSorted[ allIndexesSorted.length - 1 ];
+
+ return { first, last };
}
function sortRanges( rangesIterator ) {
diff --git a/tests/commands/mergecellscommand.js b/tests/commands/mergecellscommand.js
index d0689ece..7200beda 100644
--- a/tests/commands/mergecellscommand.js
+++ b/tests/commands/mergecellscommand.js
@@ -208,6 +208,35 @@ describe( 'MergeCellsCommand', () => {
expect( command.isEnabled ).to.be.false;
} );
+
+ it( 'should be false if more than 10 rows selected and some are in heading section', () => {
+ setData( model, modelTable( [
+ [ '0' ],
+ [ '1' ],
+ [ '2' ],
+ [ '3' ],
+ [ '4' ],
+ [ '5' ],
+ [ '6' ],
+ [ '7' ],
+ [ '8' ],
+ [ '9' ],
+ [ '10' ],
+ [ '11' ],
+ [ '12' ],
+ [ '13' ],
+ [ '14' ]
+ ], { headingRows: 10 } ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+ modelRoot.getNodeByPath( [ 0, 12, 0 ] )
+ );
+
+ expect( command.isEnabled ).to.be.false;
+ } );
} );
describe( 'execute()', () => {
diff --git a/tests/commands/removecolumncommand.js b/tests/commands/removecolumncommand.js
index 487a2f16..d352e5b7 100644
--- a/tests/commands/removecolumncommand.js
+++ b/tests/commands/removecolumncommand.js
@@ -86,6 +86,21 @@ describe( 'RemoveColumnCommand', () => {
expect( command.isEnabled ).to.be.false;
} );
+ it( 'should be false if all columns are selected - table with more than 10 columns (array sort bug)', () => {
+ setData( model, modelTable( [
+ [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12' ]
+ ] ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+ modelRoot.getNodeByPath( [ 0, 0, 12 ] )
+ );
+
+ expect( command.isEnabled ).to.be.false;
+ } );
+
it( 'should be false if selection is outside a table', () => {
setData( model, '11[]' );
diff --git a/tests/commands/removerowcommand.js b/tests/commands/removerowcommand.js
index 21ccb315..75fb74ae 100644
--- a/tests/commands/removerowcommand.js
+++ b/tests/commands/removerowcommand.js
@@ -9,22 +9,19 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils
import RemoveRowCommand from '../../src/commands/removerowcommand';
import TableSelection from '../../src/tableselection';
-import { defaultConversion, defaultSchema, modelTable, viewTable } from '../_utils/utils';
+import { modelTable, viewTable } from '../_utils/utils';
import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import TableEditing from '../../src/tableediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
describe( 'RemoveRowCommand', () => {
let editor, model, command;
- beforeEach( () => {
- return VirtualTestEditor.create( { plugins: [ TableSelection ] } )
- .then( newEditor => {
- editor = newEditor;
- model = editor.model;
- command = new RemoveRowCommand( editor );
+ beforeEach( async () => {
+ editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing, TableSelection ] } );
- defaultSchema( model.schema );
- defaultConversion( editor.conversion );
- } );
+ model = editor.model;
+ command = new RemoveRowCommand( editor );
} );
afterEach( () => {
@@ -105,6 +102,33 @@ describe( 'RemoveRowCommand', () => {
expect( command.isEnabled ).to.be.false;
} );
+
+ it( 'should be false if all the rows are selected - table with more than 10 rows (array sort bug)', () => {
+ setData( model, modelTable( [
+ [ '0' ],
+ [ '1' ],
+ [ '2' ],
+ [ '3' ],
+ [ '4' ],
+ [ '5' ],
+ [ '6' ],
+ [ '7' ],
+ [ '8' ],
+ [ '9' ],
+ [ '10' ],
+ [ '11' ],
+ [ '12' ]
+ ] ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+ modelRoot.getNodeByPath( [ 0, 12, 0 ] )
+ );
+
+ expect( command.isEnabled ).to.be.false;
+ } );
} );
describe( 'execute()', () => {
@@ -263,11 +287,11 @@ describe( 'RemoveRowCommand', () => {
[ '[]40', '41' ]
], { headingRows: 1 } ) );
- // The view should also be properly downcasted.
+ // The editing view should also be properly downcasted.
assertEqualMarkup( getViewData( editor.editing.view, { withoutSelection: true } ), viewTable( [
[ '00', '01' ],
[ '40', '41' ]
- ], { headingRows: 1 } ) );
+ ], { headingRows: 1, asWidget: true } ) );
} );
it( 'should support removing mixed heading and cell rows', () => {
@@ -339,6 +363,41 @@ describe( 'RemoveRowCommand', () => {
expect( createdBatches.size ).to.equal( 1 );
} );
+
+ it( 'should properly remove more than 10 rows selected (array sort bug)', () => {
+ setData( model, modelTable( [
+ [ '0' ],
+ [ '1' ],
+ [ '2' ],
+ [ '3' ],
+ [ '4' ],
+ [ '5' ],
+ [ '6' ],
+ [ '7' ],
+ [ '8' ],
+ [ '9' ],
+ [ '10' ],
+ [ '11' ],
+ [ '12' ],
+ [ '13' ],
+ [ '14' ]
+ ] ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+ modelRoot.getNodeByPath( [ 0, 12, 0 ] )
+ );
+
+ command.execute();
+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ '0' ],
+ [ '13' ],
+ [ '14' ]
+ ] ) );
+ } );
} );
describe( 'with entire row selected', () => {
@@ -447,8 +506,8 @@ describe( 'RemoveRowCommand', () => {
setData( model, modelTable( [
[ { rowspan: 4, contents: '00' }, { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
[ { rowspan: 2, contents: '13' }, '14' ],
- [ '22[]', '23', '24' ],
- [ '30', '31', '32', '33', '34' ]
+ [ '22[]', '24' ],
+ [ '31', '32', '33', '34' ]
] ) );
command.execute();
@@ -456,7 +515,7 @@ describe( 'RemoveRowCommand', () => {
assertEqualMarkup( getData( model ), modelTable( [
[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
[ '13', '14' ],
- [ '30', '31', '[]32', '33', '34' ]
+ [ '31', '32', '[]33', '34' ]
] ) );
} );
diff --git a/tests/commands/selectrowcommand.js b/tests/commands/selectrowcommand.js
index adcbc56a..a161d667 100644
--- a/tests/commands/selectrowcommand.js
+++ b/tests/commands/selectrowcommand.js
@@ -422,6 +422,53 @@ describe( 'SelectRowCommand', () => {
[ 0, 0 ]
] );
} );
+
+ it( 'should properly select more than 10 rows selected (array sort bug)', () => {
+ setData( model, modelTable( [
+ [ '0', 'x' ],
+ [ '1', 'x' ],
+ [ '2', 'x' ],
+ [ '3', 'x' ],
+ [ '4', 'x' ],
+ [ '5', 'x' ],
+ [ '6', 'x' ],
+ [ '7', 'x' ],
+ [ '8', 'x' ],
+ [ '9', 'x' ],
+ [ '10', 'x' ],
+ [ '11', 'x' ],
+ [ '12', 'x' ],
+ [ '13', 'x' ],
+ [ '14', 'x' ]
+ ] ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+ modelRoot.getNodeByPath( [ 0, 12, 0 ] )
+ );
+
+ command.execute();
+
+ assertSelectedCells( model, [
+ [ 0, 0 ], // '0'
+ [ 1, 1 ], // '1'
+ [ 1, 1 ], // '2'
+ [ 1, 1 ], // '3'
+ [ 1, 1 ], // '4'
+ [ 1, 1 ], // '5'
+ [ 1, 1 ], // '6'
+ [ 1, 1 ], // '7'
+ [ 1, 1 ], // '8'
+ [ 1, 1 ], // '9'
+ [ 1, 1 ], // '10'
+ [ 1, 1 ], // '11'
+ [ 1, 1 ], // '12'
+ [ 0, 0 ], // '13'
+ [ 0, 0 ] // '14
+ ] );
+ } );
} );
describe( 'with entire row selected', () => {
diff --git a/tests/commands/setheadercolumncommand.js b/tests/commands/setheadercolumncommand.js
index 1e17d39c..8871ff77 100644
--- a/tests/commands/setheadercolumncommand.js
+++ b/tests/commands/setheadercolumncommand.js
@@ -364,6 +364,25 @@ describe( 'SetHeaderColumnCommand', () => {
[ 0, 1, 1, 0 ]
] );
} );
+
+ it( 'should set it correctly in table with more than 10 columns (array sort bug)', () => {
+ setData( model, modelTable( [
+ [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14' ]
+ ] ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
+ modelRoot.getNodeByPath( [ 0, 0, 13 ] )
+ );
+
+ command.execute();
+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14' ]
+ ], { headingColumns: 14 } ) );
+ } );
} );
} );
diff --git a/tests/commands/setheaderrowcommand.js b/tests/commands/setheaderrowcommand.js
index 87be0adf..b0f5ccf1 100644
--- a/tests/commands/setheaderrowcommand.js
+++ b/tests/commands/setheaderrowcommand.js
@@ -346,6 +346,100 @@ describe( 'SetHeaderRowCommand', () => {
] );
} );
+ it( 'should set it correctly in table with more than 10 columns (array sort bug)', () => {
+ setData( model, modelTable( [
+ [ '0', 'x' ],
+ [ '1', 'x' ],
+ [ '2', 'x' ],
+ [ '3', 'x' ],
+ [ '4', 'x' ],
+ [ '5', 'x' ],
+ [ '6', 'x' ],
+ [ '7', 'x' ],
+ [ '8', 'x' ],
+ [ '9', 'x' ],
+ [ '10', 'x' ],
+ [ '11', 'x' ],
+ [ '12', 'x' ],
+ [ '13', 'x' ],
+ [ '14', 'x' ]
+ ] ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+ modelRoot.getNodeByPath( [ 0, 13, 0 ] )
+ );
+
+ command.execute();
+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ '0', 'x' ],
+ [ '1', 'x' ],
+ [ '2', 'x' ],
+ [ '3', 'x' ],
+ [ '4', 'x' ],
+ [ '5', 'x' ],
+ [ '6', 'x' ],
+ [ '7', 'x' ],
+ [ '8', 'x' ],
+ [ '9', 'x' ],
+ [ '10', 'x' ],
+ [ '11', 'x' ],
+ [ '12', 'x' ],
+ [ '13', 'x' ],
+ [ '14', 'x' ]
+ ], { headingRows: 14 } ) );
+ } );
+
+ it( 'should set it correctly in table with more than 10 columns (array sort bug, reversed selection)', () => {
+ setData( model, modelTable( [
+ [ '0', 'x' ],
+ [ '1', 'x' ],
+ [ '2', 'x' ],
+ [ '3', 'x' ],
+ [ '4', 'x' ],
+ [ '5', 'x' ],
+ [ '6', 'x' ],
+ [ '7', 'x' ],
+ [ '8', 'x' ],
+ [ '9', 'x' ],
+ [ '10', 'x' ],
+ [ '11', 'x' ],
+ [ '12', 'x' ],
+ [ '13', 'x' ],
+ [ '14', 'x' ]
+ ] ) );
+
+ const tableSelection = editor.plugins.get( TableSelection );
+ const modelRoot = model.document.getRoot();
+ tableSelection._setCellSelection(
+ modelRoot.getNodeByPath( [ 0, 13, 0 ] ),
+ modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+ );
+
+ command.execute();
+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ '0', 'x' ],
+ [ '1', 'x' ],
+ [ '2', 'x' ],
+ [ '3', 'x' ],
+ [ '4', 'x' ],
+ [ '5', 'x' ],
+ [ '6', 'x' ],
+ [ '7', 'x' ],
+ [ '8', 'x' ],
+ [ '9', 'x' ],
+ [ '10', 'x' ],
+ [ '11', 'x' ],
+ [ '12', 'x' ],
+ [ '13', 'x' ],
+ [ '14', 'x' ]
+ ], { headingRows: 14 } ) );
+ } );
+
it( 'should remove header rows in case of multiple cell selection', () => {
setData( model, modelTable( [
[ '00' ],
diff --git a/tests/converters/downcast.js b/tests/converters/downcast.js
index 01695208..dbc79a9b 100644
--- a/tests/converters/downcast.js
+++ b/tests/converters/downcast.js
@@ -4,12 +4,14 @@
*/
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import { defaultConversion, defaultSchema, modelTable, viewTable } from '../_utils/utils';
+import TableEditing from '../../src/tableediting';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { defaultConversion, defaultSchema, modelTable, viewTable } from '../_utils/utils';
function paragraphInTableCell() {
return dispatcher => dispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
@@ -41,21 +43,22 @@ describe( 'downcast converters', () => {
testUtils.createSinonSandbox();
- beforeEach( () => {
- return VirtualTestEditor.create()
- .then( newEditor => {
- editor = newEditor;
- model = editor.model;
- doc = model.document;
- root = doc.getRoot( 'main' );
- view = editor.editing.view;
-
- defaultSchema( model.schema );
- defaultConversion( editor.conversion );
- } );
- } );
-
describe( 'downcastInsertTable()', () => {
+ // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing.
+ beforeEach( () => {
+ return VirtualTestEditor.create()
+ .then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ doc = model.document;
+ root = doc.getRoot( 'main' );
+ view = editor.editing.view;
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should create table with tbody', () => {
setModelData( model, modelTable( [ [ '' ] ] ) );
@@ -351,6 +354,21 @@ describe( 'downcast converters', () => {
} );
describe( 'downcastInsertRow()', () => {
+ // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing.
+ beforeEach( () => {
+ return VirtualTestEditor.create()
+ .then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ doc = model.document;
+ root = doc.getRoot( 'main' );
+ view = editor.editing.view;
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should react to changed rows', () => {
setModelData( model, modelTable( [
[ '00', '01' ]
@@ -588,6 +606,21 @@ describe( 'downcast converters', () => {
} );
describe( 'downcastInsertCell()', () => {
+ // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing.
+ beforeEach( () => {
+ return VirtualTestEditor.create()
+ .then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ doc = model.document;
+ root = doc.getRoot( 'main' );
+ view = editor.editing.view;
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should add tableCell on proper index in tr', () => {
setModelData( model, modelTable( [
[ '00', '01' ]
@@ -732,6 +765,21 @@ describe( 'downcast converters', () => {
} );
describe( 'downcastTableHeadingColumnsChange()', () => {
+ // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing.
+ beforeEach( () => {
+ return VirtualTestEditor.create()
+ .then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ doc = model.document;
+ root = doc.getRoot( 'main' );
+ view = editor.editing.view;
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should work for adding heading columns', () => {
setModelData( model, modelTable( [
[ '00', '01' ],
@@ -891,7 +939,7 @@ describe( 'downcast converters', () => {
assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
'' +
- '' +
+ '' +
'' +
'' +
'' +
@@ -908,190 +956,198 @@ describe( 'downcast converters', () => {
} );
describe( 'downcastTableHeadingRowsChange()', () => {
- it( 'should work for adding heading rows', () => {
- setModelData( model, modelTable( [
- [ '00', '01' ],
- [ '10', '11' ],
- [ '20', '21' ]
- ] ) );
+ // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing.
+ beforeEach( () => {
+ return VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } )
+ .then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ doc = model.document;
+ root = doc.getRoot( 'main' );
+ view = editor.editing.view;
+ } );
+ } );
- const table = root.getChild( 0 );
+ // The heading rows change downcast conversion is not executed in data pipeline.
+ describe( 'editing pipeline', () => {
+ it( 'should work for adding heading rows', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01' ],
+ [ '10', '11' ],
+ [ '20', '21' ]
+ ] ) );
- model.change( writer => {
- writer.setAttribute( 'headingRows', 2, table );
+ const table = root.getChild( 0 );
+
+ model.change( writer => {
+ writer.setAttribute( 'headingRows', 2, table );
+ } );
+
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '00', '01' ],
+ [ '10', '11' ],
+ [ '20', '21' ]
+ ], { headingRows: 2, asWidget: true } ) );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ],
- [ '10', '11' ],
- [ '20', '21' ]
- ], { headingRows: 2 } ) );
- } );
+ it( 'should work for changing number of heading rows to a bigger number', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01' ],
+ [ '10', '11' ],
+ [ '20', '21' ]
+ ], { headingRows: 1 } ) );
- it( 'should work for changing number of heading rows to a bigger number', () => {
- setModelData( model, modelTable( [
- [ '00', '01' ],
- [ '10', '11' ],
- [ '20', '21' ]
- ], { headingRows: 1 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ writer.setAttribute( 'headingRows', 2, table );
+ } );
- model.change( writer => {
- writer.setAttribute( 'headingRows', 2, table );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '00', '01' ],
+ [ '10', '11' ],
+ [ '20', '21' ]
+ ], { headingRows: 2, asWidget: true } ) );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ],
- [ '10', '11' ],
- [ '20', '21' ]
- ], { headingRows: 2 } ) );
- } );
+ it( 'should work for changing number of heading rows to a smaller number', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01' ],
+ [ '10', '11' ],
+ [ '20', '21' ],
+ [ '30', '31' ]
+ ], { headingRows: 3 } ) );
- it( 'should work for changing number of heading rows to a smaller number', () => {
- setModelData( model, modelTable( [
- [ '00', '01' ],
- [ '10', '11' ],
- [ '20', '21' ],
- [ '30', '31' ]
- ], { headingRows: 3 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ writer.setAttribute( 'headingRows', 2, table );
+ } );
- model.change( writer => {
- writer.setAttribute( 'headingRows', 2, table );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '00', '01' ],
+ [ '10', '11' ],
+ [ '20', '21' ],
+ [ '30', '31' ]
+ ], { headingRows: 2, asWidget: true } ) );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ],
- [ '10', '11' ],
- [ '20', '21' ],
- [ '30', '31' ]
- ], { headingRows: 2 } ) );
- } );
+ it( 'should work for removing heading rows', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 2 } ) );
- it( 'should work for removing heading rows', () => {
- setModelData( model, modelTable( [
- [ '00', '01' ],
- [ '10', '11' ]
- ], { headingRows: 2 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ writer.removeAttribute( 'headingRows', table );
+ } );
- model.change( writer => {
- writer.removeAttribute( 'headingRows', table );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '00', '01' ],
+ [ '10', '11' ]
+ ], { asWidget: true } ) );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ],
- [ '10', '11' ]
- ] ) );
- } );
+ it( 'should work for making heading rows without tbody', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01' ],
+ [ '10', '11' ]
+ ] ) );
- it( 'should work for making heading rows without tbody', () => {
- setModelData( model, modelTable( [
- [ '00', '01' ],
- [ '10', '11' ]
- ] ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ writer.setAttribute( 'headingRows', 2, table );
+ } );
- model.change( writer => {
- writer.setAttribute( 'headingRows', 2, table );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '00', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 2, asWidget: true } ) );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ],
- [ '10', '11' ]
- ], { headingRows: 2 } ) );
- } );
+ it( 'should be possible to overwrite', () => {
+ editor.conversion.attributeToAttribute( {
+ model: 'headingRows',
+ view: 'headingRows',
+ converterPriority: 'high'
+ } );
+ setModelData( model, modelTable( [ [ '00' ] ] ) );
- it( 'should be possible to overwrite', () => {
- editor.conversion.attributeToAttribute( { model: 'headingRows', view: 'headingRows', converterPriority: 'high' } );
- setModelData( model, modelTable( [ [ '00' ] ] ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ writer.setAttribute( 'headingRows', 1, table );
+ } );
- model.change( writer => {
- writer.setAttribute( 'headingRows', 1, table );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '00' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
- '' +
- '' +
- '' +
- '00 |
' +
- '' +
- '
' +
- ' '
- );
- } );
+ it( 'should work with adding table rows at the beginning of a table', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 1 } ) );
- it( 'should work with adding table rows at the beginning of a table', () => {
- setModelData( model, modelTable( [
- [ '00', '01' ],
- [ '10', '11' ]
- ], { headingRows: 1 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ writer.setAttribute( 'headingRows', 2, table );
- model.change( writer => {
- writer.setAttribute( 'headingRows', 2, table );
+ const tableRow = writer.createElement( 'tableRow' );
- const tableRow = writer.createElement( 'tableRow' );
+ writer.insert( tableRow, table, 0 );
+ writer.insertElement( 'tableCell', tableRow, 'end' );
+ writer.insertElement( 'tableCell', tableRow, 'end' );
+ } );
- writer.insert( tableRow, table, 0 );
- writer.insertElement( 'tableCell', tableRow, 'end' );
- writer.insertElement( 'tableCell', tableRow, 'end' );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '', '' ],
+ [ '00', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 2, asWidget: true } ) );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '', '' ],
- [ '00', '01' ],
- [ '10', '11' ]
- ], { headingRows: 2 } ) );
- } );
-
- it( 'should work with adding a table row and expanding heading', () => {
- setModelData( model, modelTable( [
- [ '00', '01' ],
- [ '10', '11' ],
- [ '20', '21' ]
- ], { headingRows: 1 } ) );
-
- const table = root.getChild( 0 );
-
- model.change( writer => {
- writer.setAttribute( 'headingRows', 2, table );
+ it( 'should work with adding a table row and expanding heading', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01' ],
+ [ '10', '11' ],
+ [ '20', '21' ]
+ ], { headingRows: 1 } ) );
- const tableRow = writer.createElement( 'tableRow' );
+ const table = root.getChild( 0 );
- writer.insert( tableRow, table, 1 );
- writer.insertElement( 'tableCell', tableRow, 'end' );
- writer.insertElement( 'tableCell', tableRow, 'end' );
- } );
+ model.change( writer => {
+ writer.setAttribute( 'headingRows', 2, table );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ],
- [ '', '' ],
- [ '10', '11' ],
- [ '20', '21' ]
- ], { headingRows: 2 } ) );
- } );
+ const tableRow = writer.createElement( 'tableRow' );
- describe( 'options.asWidget=true', () => {
- beforeEach( () => {
- return VirtualTestEditor.create()
- .then( newEditor => {
- editor = newEditor;
- model = editor.model;
- doc = model.document;
- root = doc.getRoot( 'main' );
- view = editor.editing.view;
+ writer.insert( tableRow, table, 1 );
+ writer.insertElement( 'tableCell', tableRow, 'end' );
+ writer.insertElement( 'tableCell', tableRow, 'end' );
+ } );
- defaultSchema( model.schema );
- defaultConversion( editor.conversion, true );
- } );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '00', '01' ],
+ [ '', '' ],
+ [ '10', '11' ],
+ [ '20', '21' ]
+ ], { headingRows: 2, asWidget: true } ) );
} );
it( 'should create renamed cell as a widget', () => {
@@ -1122,107 +1178,254 @@ describe( 'downcast converters', () => {
} );
describe( 'downcastRemoveRow()', () => {
- it( 'should react to removed row from the beginning of a tbody', () => {
- setModelData( model, modelTable( [
- [ '00[]', '01' ],
- [ '10', '11' ]
- ] ) );
+ // The beforeEach is duplicated due to ckeditor/ckeditor5#6574. New test are written using TableEditing.
+ beforeEach( async () => {
+ editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } );
- const table = root.getChild( 0 );
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ view = editor.editing.view;
+ } );
- model.change( writer => {
- writer.remove( table.getChild( 1 ) );
+ // The remove row downcast conversion is not executed in data pipeline.
+ describe( 'editing pipeline', () => {
+ it( 'should react to removed row from the beginning of a body rows (no heading rows)', () => {
+ setModelData( model, modelTable( [
+ [ '00[]', '01' ],
+ [ '10', '11' ]
+ ] ) );
+
+ const table = root.getChild( 0 );
+
+ model.change( writer => {
+ writer.remove( table.getChild( 1 ) );
+ } );
+
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '00' +
+ ' | ' +
+ '' +
+ '01' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ]
- ] ) );
- } );
+ it( 'should react to removed row form the end of a body rows (no heading rows)', () => {
+ setModelData( model, modelTable( [
+ [ '00[]', '01' ],
+ [ '10', '11' ]
+ ] ) );
- it( 'should react to removed row form the end of a tbody', () => {
- setModelData( model, modelTable( [
- [ '00[]', '01' ],
- [ '10', '11' ]
- ] ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ writer.remove( table.getChild( 0 ) );
+ } );
- model.change( writer => {
- writer.remove( table.getChild( 0 ) );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '10' +
+ ' | ' +
+ '' +
+ '11' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '10', '11' ]
- ] ) );
- } );
+ it( 'should react to removed row from the beginning of a heading rows (no body rows)', () => {
+ setModelData( model, modelTable( [
+ [ '00[]', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 2 } ) );
- it( 'should react to removed row from the beginning of a thead', () => {
- setModelData( model, modelTable( [
- [ '00[]', '01' ],
- [ '10', '11' ]
- ], { headingRows: 2 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ // Removing row from a heading section changes requires changing heading rows attribute.
+ writer.setAttribute( 'headingRows', 1, table );
+ writer.remove( table.getChild( 0 ) );
+ } );
- model.change( writer => {
- writer.remove( table.getChild( 1 ) );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '10' +
+ ' | ' +
+ '' +
+ '11' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ]
- ], { headingRows: 2 } ) );
- } );
+ it( 'should react to removed row form the end of a heading rows (no body rows)', () => {
+ setModelData( model, modelTable( [
+ [ '00[]', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 2 } ) );
- it( 'should react to removed row form the end of a thead', () => {
- setModelData( model, modelTable( [
- [ '00[]', '01' ],
- [ '10', '11' ]
- ], { headingRows: 2 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ // Removing row from a heading section changes requires changing heading rows attribute.
+ writer.setAttribute( 'headingRows', 1, table );
+ writer.remove( table.getChild( 1 ) );
+ } );
- model.change( writer => {
- writer.remove( table.getChild( 0 ) );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '00' +
+ ' | ' +
+ '' +
+ '01' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '10', '11' ]
- ], { headingRows: 2 } ) );
- } );
+ it( 'should react to removed row form the end of a heading rows (first cell in body has colspan)', () => {
+ setModelData( model, modelTable( [
+ [ '00[]', '01', '02', '03' ],
+ [ { rowspan: 2, colspan: 2, contents: '10' }, '12', '13' ],
+ [ '22', '23' ]
+ ], { headingRows: 1 } ) );
- it( 'should remove empty thead section if a last row was removed from thead', () => {
- setModelData( model, modelTable( [
- [ '00[]', '01' ],
- [ '10', '11' ]
- ], { headingRows: 1 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ // Removing row from a heading section changes requires changing heading rows attribute.
+ writer.remove( table.getChild( 0 ) );
+ writer.setAttribute( 'headingRows', 0, table );
+ } );
- model.change( writer => {
- writer.setAttribute( 'headingRows', 0, table );
- writer.remove( table.getChild( 0 ) );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '10' +
+ ' | ' +
+ '' +
+ '12' +
+ ' | ' +
+ '' +
+ '13' +
+ ' | ' +
+ '
' +
+ '' +
+ '' +
+ '22' +
+ ' | ' +
+ '' +
+ '23' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '10', '11' ]
- ] ) );
- } );
+ it( 'should remove empty thead if a last row was removed from a heading rows (has heading and body)', () => {
+ setModelData( model, modelTable( [
+ [ '00[]', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 1 } ) );
- it( 'should remove empty tbody section if a last row was removed from tbody', () => {
- setModelData( model, modelTable( [
- [ '00[]', '01' ],
- [ '10', '11' ]
- ], { headingRows: 1 } ) );
+ const table = root.getChild( 0 );
- const table = root.getChild( 0 );
+ model.change( writer => {
+ // Removing row from a heading section changes requires changing heading rows attribute.
+ writer.removeAttribute( 'headingRows', table );
+ writer.remove( table.getChild( 0 ) );
+ } );
- model.change( writer => {
- writer.remove( table.getChild( 1 ) );
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '10' +
+ ' | ' +
+ '' +
+ '11' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
} );
- assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
- [ '00', '01' ]
- ], { headingRows: 1 } ) );
+ it( 'should remove empty tbody if a last row was removed a body rows (has heading and body)', () => {
+ setModelData( model, modelTable( [
+ [ '00[]', '01' ],
+ [ '10', '11' ]
+ ], { headingRows: 1 } ) );
+
+ const table = root.getChild( 0 );
+
+ model.change( writer => {
+ writer.remove( table.getChild( 1 ) );
+ } );
+
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ),
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '00' +
+ ' | ' +
+ '' +
+ '01' +
+ ' | ' +
+ '
' +
+ '' +
+ '
' +
+ ' '
+ );
+ } );
} );
} );
diff --git a/tests/tableutils.js b/tests/tableutils.js
index ae1c4f56..870412b1 100644
--- a/tests/tableutils.js
+++ b/tests/tableutils.js
@@ -8,37 +8,53 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model
import { defaultConversion, defaultSchema, modelTable } from './_utils/utils';
+import TableEditing from '../src/tableediting';
import TableUtils from '../src/tableutils';
import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
describe( 'TableUtils', () => {
let editor, model, root, tableUtils;
- beforeEach( () => {
- return ModelTestEditor.create( {
- plugins: [ TableUtils ]
- } ).then( newEditor => {
- editor = newEditor;
- model = editor.model;
- root = model.document.getRoot( 'main' );
- tableUtils = editor.plugins.get( TableUtils );
-
- defaultSchema( model.schema );
- defaultConversion( editor.conversion );
- } );
- } );
-
afterEach( () => {
return editor.destroy();
} );
describe( '#pluginName', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should provide plugin name', () => {
expect( TableUtils.pluginName ).to.equal( 'TableUtils' );
} );
} );
describe( 'getCellLocation()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should return proper table cell location', () => {
setData( model, modelTable( [
[ { rowspan: 2, colspan: 2, contents: '00[]' }, '02' ],
@@ -52,6 +68,20 @@ describe( 'TableUtils', () => {
} );
describe( 'insertRows()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should insert row in given table at given index', () => {
setData( model, modelTable( [
[ '11[]', '12' ],
@@ -192,6 +222,20 @@ describe( 'TableUtils', () => {
} );
describe( 'insertColumns()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should insert column in given table at given index', () => {
setData( model, modelTable( [
[ '11[]', '12' ],
@@ -370,7 +414,7 @@ describe( 'TableUtils', () => {
], { headingColumns: 4 } ) );
} );
- it( 'should properly insert column while table has rowspanned cells', () => {
+ it( 'should properly insert column while table has row-spanned cells', () => {
setData( model, modelTable( [
[ { rowspan: 4, contents: '00[]' }, { rowspan: 2, contents: '01' }, '02' ],
[ '12' ],
@@ -390,6 +434,20 @@ describe( 'TableUtils', () => {
} );
describe( 'splitCellVertically()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should split table cell to given table cells number', () => {
setData( model, modelTable( [
[ '00', '01', '02' ],
@@ -534,6 +592,20 @@ describe( 'TableUtils', () => {
} );
describe( 'splitCellHorizontally()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should split table cell to default table cells number', () => {
setData( model, modelTable( [
[ '00', '01', '02' ],
@@ -570,7 +642,7 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should properly update rowspanned cells overlapping selected cell', () => {
+ it( 'should properly update row-spanned cells overlapping selected cell', () => {
setData( model, modelTable( [
[ { rowspan: 2, contents: '00' }, '01', { rowspan: 3, contents: '02' } ],
[ '[]11' ],
@@ -588,7 +660,7 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should split rowspanned cell', () => {
+ it( 'should split row-spanned cell', () => {
setData( model, modelTable( [
[ '00', { rowspan: 2, contents: '01[]' } ],
[ '10' ],
@@ -606,7 +678,7 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should copy colspan while splitting rowspanned cell', () => {
+ it( 'should copy colspan while splitting row-spanned cell', () => {
setData( model, modelTable( [
[ '00', { rowspan: 2, colspan: 2, contents: '01[]' } ],
[ '10' ],
@@ -652,7 +724,7 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should split rowspanned cell and updated other cells rowspan when splitting to bigger number of cells', () => {
+ it( 'should split row-spanned cell and updated other cells rowspan when splitting to bigger number of cells', () => {
setData( model, modelTable( [
[ '00', { rowspan: 2, contents: '01[]' } ],
[ '10' ],
@@ -671,7 +743,7 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should split rowspanned & colspaned cell', () => {
+ it( 'should split row-spanned & col-spanned cell', () => {
setData( model, modelTable( [
[ '00', { colspan: 2, contents: '01[]' } ],
[ '10', '11' ]
@@ -709,6 +781,20 @@ describe( 'TableUtils', () => {
} );
describe( 'getColumns()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should return proper number of columns', () => {
setData( model, modelTable( [
[ '00', { colspan: 3, contents: '01' }, '04' ]
@@ -719,6 +805,20 @@ describe( 'TableUtils', () => {
} );
describe( 'getRows()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
it( 'should return proper number of columns for simple table', () => {
setData( model, modelTable( [
[ '00', '01' ],
@@ -749,6 +849,17 @@ describe( 'TableUtils', () => {
} );
describe( 'removeRows()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ Paragraph, TableEditing, TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+ } );
+ } );
+
describe( 'single row', () => {
it( 'should remove a given row from a table start', () => {
setData( model, modelTable( [
@@ -794,11 +905,57 @@ describe( 'TableUtils', () => {
} );
it( 'should decrease rowspan of table cells from previous rows', () => {
+ // +----+----+----+----+----+
+ // | 00 | 01 | 02 | 03 | 04 |
+ // +----+ + + + +
+ // | 10 | | | | |
+ // +----+----+ + + +
+ // | 20 | 21 | | | |
+ // +----+----+----+ + +
+ // | 30 | 31 | 32 | | |
+ // +----+----+----+----+ +
+ // | 40 | 41 | 42 | 43 | |
+ // +----+----+----+----+----+
+ // | 50 | 51 | 52 | 53 | 54 |
+ // +----+----+----+----+----+
+ setData( model, modelTable( [
+ [ '00', { contents: '01', rowspan: 2 }, { contents: '02', rowspan: 3 }, { contents: '03', rowspan: 4 },
+ { contents: '04', rowspan: 5 } ],
+ [ '10' ],
+ [ '20', '21' ],
+ [ '30', '31', '32' ],
+ [ '40', '41', '42', '43' ],
+ [ '50', '51', '52', '53', '54' ]
+ ] ) );
+
+ tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 1 } );
+
+ // +----+----+----+----+----+
+ // | 00 | 01 | 02 | 03 | 04 |
+ // +----+----+ + + +
+ // | 20 | 21 | | | |
+ // +----+----+----+ + +
+ // | 30 | 31 | 32 | | |
+ // +----+----+----+----+ +
+ // | 40 | 41 | 42 | 43 | |
+ // +----+----+----+----+----+
+ // | 50 | 51 | 52 | 53 | 54 |
+ // +----+----+----+----+----+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ '00', '01', { contents: '02', rowspan: 2 }, { contents: '03', rowspan: 3 }, { contents: '04', rowspan: 4 } ],
+ [ '20', '21' ],
+ [ '30', '31', '32' ],
+ [ '40', '41', '42', '43' ],
+ [ '50', '51', '52', '53', '54' ]
+ ] ) );
+ } );
+
+ it( 'should decrease rowspan of table cells from previous rows (row-spanned cells on different rows)', () => {
setData( model, modelTable( [
[ { rowspan: 4, contents: '00' }, { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
[ { rowspan: 2, contents: '13' }, '14' ],
- [ '22', '23', '24' ],
- [ '30', '31', '32', '33', '34' ]
+ [ '22', '24' ],
+ [ '31', '32', '33', '34' ]
] ) );
tableUtils.removeRows( root.getChild( 0 ), { at: 2 } );
@@ -806,11 +963,11 @@ describe( 'TableUtils', () => {
assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
[ '13', '14' ],
- [ '30', '31', '32', '33', '34' ]
+ [ '31', '32', '33', '34' ]
] ) );
} );
- it( 'should move rowspaned cells to row below removing it\'s row', () => {
+ it( 'should move row-spanned cells to a row below removing it\'s row', () => {
setData( model, modelTable( [
[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02' ],
[ '12' ],
@@ -826,6 +983,21 @@ describe( 'TableUtils', () => {
[ '30', '31', '32' ]
] ) );
} );
+
+ it( 'should move row-spanned cells to a row below removing it\'s row (other cell is overlapping removed row)', () => {
+ setData( model, modelTable( [
+ [ '00', { rowspan: 3, contents: '01' }, '02', '03', '04' ],
+ [ '10', { rowspan: 2, contents: '12' }, '13', '14' ],
+ [ '20', '23', '24' ]
+ ] ) );
+
+ tableUtils.removeRows( root.getChild( 0 ), { at: 1 } );
+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ '00', { rowspan: 2, contents: '01' }, '02', '03', '04' ],
+ [ '20', '12', '23', '24' ]
+ ] ) );
+ } );
} );
describe( 'many rows', () => {
@@ -924,21 +1096,85 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should properly calculate truncated rowspans', () => {
+ it( 'should move row-spanned cells to a row after removed rows section', () => {
+ setData( model, modelTable( [
+ [ '00', '01', '02', '03' ],
+ [ { rowspan: 4, contents: '10' }, { rowspan: 3, contents: '11' }, { rowspan: 2, contents: '12' }, '13' ],
+ [ { rowspan: 3, contents: '23' } ],
+ [ '32' ],
+ [ '41', '42' ]
+ ] ) );
+
+ tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 2 } );
+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ '00', '01', '02', '03' ],
+ [ { rowspan: 2, contents: '10' }, '11', '32', { rowspan: 2, contents: '23' } ],
+ [ '41', '42' ]
+ ] ) );
+ } );
+
+ it( 'should decrease rowspan of table cells from rows before removed rows section', () => {
setData( model, modelTable( [
- [ '00', { contents: '01', rowspan: 3 } ],
+ [ { rowspan: 4, contents: '00' }, { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03' ],
+ [ '13' ],
+ [ '22', '23' ],
+ [ '31', '32', '33' ]
+ ] ) );
+
+ tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 2 } );
+
+ assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+ [ { rowspan: 2, contents: '00' }, '01', '02', '03' ],
+ [ '31', '32', '33' ]
+ ] ) );
+ } );
+
+ it( 'should decrease rowspan of table cells from previous rows', () => {
+ // +----+----+----+----+----+
+ // | 00 | 01 | 02 | 03 | 04 |
+ // +----+ + + + +
+ // | 10 | | | | |
+ // +----+----+ + + +
+ // | 20 | 21 | | | |
+ // +----+----+----+ + +
+ // | 30 | 31 | 32 | | |
+ // +----+----+----+----+ +
+ // | 40 | 41 | 42 | 43 | |
+ // +----+----+----+----+----+
+ // | 50 | 51 | 52 | 53 | 54 |
+ // +----+----+----+----+----+
+ setData( model, modelTable( [
+ [ '00', { contents: '01', rowspan: 2 }, { contents: '02', rowspan: 3 }, { contents: '03', rowspan: 4 },
+ { contents: '04', rowspan: 5 } ],
[ '10' ],
- [ '20' ]
+ [ '20', '21' ],
+ [ '30', '31', '32' ],
+ [ '40', '41', '42', '43' ],
+ [ '50', '51', '52', '53', '54' ]
] ) );
- tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } );
+ tableUtils.removeRows( root.getChild( 0 ), { at: 2, rows: 2 } );
+ // +----+----+----+----+----+
+ // | 00 | 01 | 02 | 03 | 04 |
+ // +----+ + + + +
+ // | 10 | | | | |
+ // +----+----+----+----+ +
+ // | 40 | 41 | 42 | 43 | |
+ // +----+----+----+----+----+
+ // | 50 | 51 | 52 | 53 | 54 |
+ // +----+----+----+----+----+
assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
- [ '20', '01' ]
+ [ '00', { contents: '01', rowspan: 2 }, { contents: '02', rowspan: 2 }, { contents: '03', rowspan: 2 },
+ { contents: '04', rowspan: 3 } ],
+ [ '10' ],
+ [ '40', '41', '42', '43' ],
+ [ '50', '51', '52', '53', '54' ]
] ) );
} );
- it( 'should create one undo step (1 batch)', () => {
+ it( 'should re-use batch to create one undo step', () => {
setData( model, modelTable( [
[ '00', '01' ],
[ '10', '11' ],
@@ -954,7 +1190,9 @@ describe( 'TableUtils', () => {
createdBatches.add( operation.batch );
} );
- tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } );
+ const batch = model.createBatch();
+
+ tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2, batch } );
expect( createdBatches.size ).to.equal( 1 );
} );
@@ -962,6 +1200,20 @@ describe( 'TableUtils', () => {
} );
describe( 'removeColumns()', () => {
+ beforeEach( () => {
+ return ModelTestEditor.create( {
+ plugins: [ TableUtils ]
+ } ).then( newEditor => {
+ editor = newEditor;
+ model = editor.model;
+ root = model.document.getRoot( 'main' );
+ tableUtils = editor.plugins.get( TableUtils );
+
+ defaultSchema( model.schema );
+ defaultConversion( editor.conversion );
+ } );
+ } );
+
describe( 'single row', () => {
it( 'should remove a given column', () => {
setData( model, modelTable( [
@@ -1076,7 +1328,7 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should remove column if other column is rowspanned (last column)', () => {
+ it( 'should remove column if other column is row-spanned (last column)', () => {
setData( model, modelTable( [
[ '00', { rowspan: 2, contents: '01' } ],
[ '10' ]
@@ -1089,7 +1341,7 @@ describe( 'TableUtils', () => {
] ) );
} );
- it( 'should remove column if other column is rowspanned (first column)', () => {
+ it( 'should remove column if other column is row-spanned (first column)', () => {
setData( model, modelTable( [
[ { rowspan: 2, contents: '00' }, '01' ],
[ '11' ]