Przeglądaj źródła

Merge pull request #6834 from ckeditor/i/6785-table-walker-tuning

Other (table): Refactor values returned by the `TableWalker` iterator. Closes #6785.

Other (table): Add `row`, `startColumn`, and `endColumn` options to `TableWalker` constructor. See #6785.

MINOR BREAKING CHANGE (table): The values returned by the `TableWalker` iterator have changed. See #6785.
Maciej 5 lat temu
rodzic
commit
65cfa13ec9

+ 2 - 2
packages/ckeditor5-table/src/commands/mergecellcommand.js

@@ -227,7 +227,7 @@ function getVerticalCell( tableCell, direction ) {
 	const currentCellData = tableMap.find( value => value.cell === tableCell );
 	const mergeColumn = currentCellData.column;
 
-	const cellToMergeData = tableMap.find( ( { row, rowspan, column } ) => {
+	const cellToMergeData = tableMap.find( ( { row, cellHeight, column } ) => {
 		if ( column !== mergeColumn ) {
 			return false;
 		}
@@ -237,7 +237,7 @@ function getVerticalCell( tableCell, direction ) {
 			return row === rowOfCellToMerge;
 		} else {
 			// If merging a cell above calculate if it spans to mergeRow.
-			return rowOfCellToMerge === row + rowspan;
+			return rowOfCellToMerge === row + cellHeight;
 		}
 	} );
 

+ 3 - 4
packages/ckeditor5-table/src/commands/selectcolumncommand.js

@@ -42,6 +42,7 @@ export default class SelectColumnCommand extends Command {
 		const referenceCells = getSelectionAffectedTableCells( model.document.selection );
 		const firstCell = referenceCells[ 0 ];
 		const lastCell = referenceCells.pop();
+		const table = findAncestor( 'table', firstCell );
 
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 		const startLocation = tableUtils.getCellLocation( firstCell );
@@ -52,10 +53,8 @@ export default class SelectColumnCommand extends Command {
 
 		const rangesToSelect = [];
 
-		for ( const cellInfo of new TableWalker( findAncestor( 'table', firstCell ) ) ) {
-			if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
-				rangesToSelect.push( model.createRangeOn( cellInfo.cell ) );
-			}
+		for ( const cellInfo of new TableWalker( table, { startColumn, endColumn } ) ) {
+			rangesToSelect.push( model.createRangeOn( cellInfo.cell ) );
 		}
 
 		model.change( writer => {

+ 26 - 31
packages/ckeditor5-table/src/converters/downcast.js

@@ -54,8 +54,8 @@ export function downcastInsertTable( options = {} ) {
 		// Cache for created table rows.
 		const viewRows = new Map();
 
-		for ( const tableWalkerValue of tableWalker ) {
-			const { row, cell } = tableWalkerValue;
+		for ( const tableSlot of tableWalker ) {
+			const { row, cell } = tableSlot;
 
 			const tableSection = getOrCreateTableSection( getSectionName( row, tableAttributes ), tableElement, conversionApi );
 			const tableRow = table.getChild( row );
@@ -68,7 +68,7 @@ export function downcastInsertTable( options = {} ) {
 
 			const insertPosition = conversionApi.writer.createPositionAt( trElement, 'end' );
 
-			createViewTableCellElement( tableWalkerValue, tableAttributes, insertPosition, conversionApi, options );
+			createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
 		}
 
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
@@ -100,7 +100,7 @@ export function downcastInsertRow( options = {} ) {
 
 		const row = table.getChildIndex( tableRow );
 
-		const tableWalker = new TableWalker( table, { startRow: row, endRow: row } );
+		const tableWalker = new TableWalker( table, { row } );
 
 		const tableAttributes = {
 			headingRows: table.getAttribute( 'headingRows' ) || 0,
@@ -110,18 +110,18 @@ export function downcastInsertRow( options = {} ) {
 		// Cache for created table rows.
 		const viewRows = new Map();
 
-		for ( const tableWalkerValue of tableWalker ) {
+		for ( const tableSlot of tableWalker ) {
 			const tableSection = getOrCreateTableSection( getSectionName( row, tableAttributes ), tableElement, conversionApi );
 
 			const trElement = viewRows.get( row ) || createTr( tableRow, row, tableSection, conversionApi );
 			viewRows.set( row, trElement );
 
 			// Consume table cell - it will be always consumed as we convert whole row at once.
-			conversionApi.consumable.consume( tableWalkerValue.cell, 'insert' );
+			conversionApi.consumable.consume( tableSlot.cell, 'insert' );
 
 			const insertPosition = conversionApi.writer.createPositionAt( trElement, 'end' );
 
-			createViewTableCellElement( tableWalkerValue, tableAttributes, insertPosition, conversionApi, options );
+			createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
 		}
 	} );
 }
@@ -146,7 +146,7 @@ export function downcastInsertCell( options = {} ) {
 		const table = tableRow.parent;
 		const rowIndex = table.getChildIndex( tableRow );
 
-		const tableWalker = new TableWalker( table, { startRow: rowIndex, endRow: rowIndex } );
+		const tableWalker = new TableWalker( table, { row: rowIndex } );
 
 		const tableAttributes = {
 			headingRows: table.getAttribute( 'headingRows' ) || 0,
@@ -154,12 +154,12 @@ export function downcastInsertCell( options = {} ) {
 		};
 
 		// We need to iterate over a table in order to get proper row & column values from a walker
-		for ( const tableWalkerValue of tableWalker ) {
-			if ( tableWalkerValue.cell === tableCell ) {
+		for ( const tableSlot of tableWalker ) {
+			if ( tableSlot.cell === tableCell ) {
 				const trElement = conversionApi.mapper.toViewElement( tableRow );
 				const insertPosition = conversionApi.writer.createPositionAt( trElement, tableRow.getChildIndex( tableCell ) );
 
-				createViewTableCellElement( tableWalkerValue, tableAttributes, insertPosition, conversionApi, options );
+				createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options );
 
 				// No need to iterate further.
 				return;
@@ -228,8 +228,8 @@ export function downcastTableHeadingRowsChange( options = {} ) {
 				headingColumns: table.getAttribute( 'headingColumns' ) || 0
 			};
 
-			for ( const tableWalkerValue of tableWalker ) {
-				renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conversionApi, asWidget );
+			for ( const tableSlot of tableWalker ) {
+				renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget );
 			}
 		}
 
@@ -270,13 +270,8 @@ export function downcastTableHeadingColumnsChange( options = {} ) {
 
 		const lastColumnToCheck = ( oldColumns > newColumns ? oldColumns : newColumns ) - 1;
 
-		for ( const tableWalkerValue of new TableWalker( table ) ) {
-			// Skip cells that were not in heading section before and after the change.
-			if ( tableWalkerValue.column > lastColumnToCheck ) {
-				continue;
-			}
-
-			renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conversionApi, asWidget );
+		for ( const tableSlot of new TableWalker( table, { endColumn: lastColumnToCheck } ) ) {
+			renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget );
 		}
 	} );
 }
@@ -348,15 +343,15 @@ function renameViewTableCell( tableCell, desiredCellElementName, conversionApi,
 
 // Renames a table cell element in the view according to its location in the table.
 //
-// @param {module:table/tablewalker~TableWalkerValue} tableWalkerValue
+// @param {module:table/tablewalker~TableSlot} tableSlot
 // @param {{headingColumns, headingRows}} tableAttributes
 // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
 // @param {Boolean} asWidget
-function renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conversionApi, asWidget ) {
-	const { cell } = tableWalkerValue;
+function renameViewTableCellIfRequired( tableSlot, tableAttributes, conversionApi, asWidget ) {
+	const { cell } = tableSlot;
 
 	// Check whether current columnIndex is overlapped by table cells from previous rows.
-	const desiredCellElementName = getCellElementName( tableWalkerValue, tableAttributes );
+	const desiredCellElementName = getCellElementName( tableSlot, tableAttributes );
 
 	const viewCell = conversionApi.mapper.toViewElement( cell );
 
@@ -369,18 +364,18 @@ function renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conve
 
 // Creates a table cell element in the view.
 //
-// @param {module:table/tablewalker~TableWalkerValue} tableWalkerValue
+// @param {module:table/tablewalker~TableSlot} tableSlot
 // @param {module:engine/view/position~Position} insertPosition
 // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
-function createViewTableCellElement( tableWalkerValue, tableAttributes, insertPosition, conversionApi, options ) {
+function createViewTableCellElement( tableSlot, tableAttributes, insertPosition, conversionApi, options ) {
 	const asWidget = options && options.asWidget;
-	const cellElementName = getCellElementName( tableWalkerValue, tableAttributes );
+	const cellElementName = getCellElementName( tableSlot, tableAttributes );
 
 	const cellElement = asWidget ?
 		toWidgetEditable( conversionApi.writer.createEditableElement( cellElementName ), conversionApi.writer ) :
 		conversionApi.writer.createContainerElement( cellElementName );
 
-	const tableCell = tableWalkerValue.cell;
+	const tableCell = tableSlot.cell;
 
 	const firstChild = tableCell.getChild( 0 );
 	const isSingleParagraph = tableCell.childCount === 1 && firstChild.name === 'paragraph';
@@ -436,11 +431,11 @@ function createTr( tableRow, rowIndex, tableSection, conversionApi ) {
 
 // Returns `th` for heading cells and `td` for other cells for the current table walker value.
 //
-// @param {module:table/tablewalker~TableWalkerValue} tableWalkerValue
+// @param {module:table/tablewalker~TableSlot} tableSlot
 // @param {{headingColumns, headingRows}} tableAttributes
 // @returns {String}
-function getCellElementName( tableWalkerValue, tableAttributes ) {
-	const { row, column } = tableWalkerValue;
+function getCellElementName( tableSlot, tableAttributes ) {
+	const { row, column } = tableSlot;
 	const { headingColumns, headingRows } = tableAttributes;
 
 	// Column heading are all tableCells in the first `columnHeading` rows.

+ 4 - 4
packages/ckeditor5-table/src/converters/table-layout-post-fixer.js

@@ -350,9 +350,9 @@ function findCellsToTrim( table ) {
 
 	const cellsToTrim = [];
 
-	for ( const { row, rowspan, cell } of new TableWalker( table ) ) {
+	for ( const { row, cell, cellHeight } of new TableWalker( table ) ) {
 		// Skip cells that do not expand over its row.
-		if ( rowspan < 2 ) {
+		if ( cellHeight < 2 ) {
 			continue;
 		}
 
@@ -362,7 +362,7 @@ function findCellsToTrim( table ) {
 		const rowLimit = isInHeader ? headingRows : maxRows;
 
 		// If table cell expands over its limit reduce it height to proper value.
-		if ( row + rowspan > rowLimit ) {
+		if ( row + cellHeight > rowLimit ) {
 			const newRowspan = rowLimit - row;
 
 			cellsToTrim.push( { cell, rowspan: newRowspan } );
@@ -380,7 +380,7 @@ function getRowsLengths( table ) {
 	// TableWalker will not provide items for the empty rows, we need to pre-fill this array.
 	const lengths = new Array( table.childCount ).fill( 0 );
 
-	for ( const { row } of new TableWalker( table, { includeSpanned: true } ) ) {
+	for ( const { row } of new TableWalker( table, { includeAllSlots: true } ) ) {
 		lengths[ row ]++;
 	}
 

+ 25 - 41
packages/ckeditor5-table/src/tableclipboard.js

@@ -205,7 +205,7 @@ export default class TableClipboard extends Plugin {
 				endColumn: Math.min( selectionWidth - 1, pasteWidth - 1 )
 			};
 
-			pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer, tableUtils );
+			pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer );
 
 			const pastedDimensions = {
 				width: pasteWidth,
@@ -250,14 +250,16 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
 	const selectedTableMap = [ ...new TableWalker( selectedTable, {
 		startRow: firstRowOfSelection,
 		endRow: lastRowOfSelection,
-		includeSpanned: true
+		startColumn: firstColumnOfSelection,
+		endColumn: lastColumnOfSelection,
+		includeAllSlots: true
 	} ) ];
 
 	// Selection must be set to pasted cells (some might be removed or new created).
 	const cellsToSelect = [];
 
-	// Store previous cell in order to insert a new table cells after it (if required).
-	let previousCellInRow;
+	// Store next cell insert position.
+	let insertPosition;
 
 	// Content table replace cells algorithm iterates over a selected table fragment and:
 	//
@@ -265,26 +267,19 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
 	// - Inserts cell from a pasted table for a matched slots.
 	//
 	// This ensures proper table geometry after the paste
-	for ( const { row, column, cell, isSpanned } of selectedTableMap ) {
-		if ( column === 0 ) {
-			previousCellInRow = null;
-		}
-
-		// Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785.
-		if ( column < firstColumnOfSelection || column > lastColumnOfSelection ) {
-			// Only update the previousCellInRow for non-spanned slots.
-			if ( !isSpanned ) {
-				previousCellInRow = cell;
-			}
+	for ( const tableSlot of selectedTableMap ) {
+		const { row, column, cell, isAnchor } = tableSlot;
 
-			continue;
+		// Save the insert position for current row start.
+		if ( column === firstColumnOfSelection ) {
+			insertPosition = tableSlot.getPositionBefore();
 		}
 
 		// If the slot is occupied by a cell in a selected table - remove it.
 		// The slot of this cell will be either:
 		// - Replaced by a pasted table cell.
 		// - Spanned by a previously pasted table cell.
-		if ( !isSpanned ) {
+		if ( isAnchor ) {
 			writer.remove( cell );
 		}
 
@@ -305,17 +300,10 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
 		// Trim the cell if it's row/col-spans would exceed selection area.
 		trimTableCellIfNeeded( cellToInsert, row, column, lastRowOfSelection, lastColumnOfSelection, writer );
 
-		let insertPosition;
-
-		if ( !previousCellInRow ) {
-			insertPosition = writer.createPositionAt( selectedTable.getChild( row ), 0 );
-		} else {
-			insertPosition = writer.createPositionAfter( previousCellInRow );
-		}
-
 		writer.insert( cellToInsert, insertPosition );
 		cellsToSelect.push( cellToInsert );
-		previousCellInRow = cellToInsert;
+
+		insertPosition = writer.createPositionAfter( cellToInsert );
 	}
 
 	writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
@@ -460,7 +448,7 @@ function doHorizontalSplit( table, splitRow, limitColumns, writer, startRow = 0
 	const overlappingCells = getVerticallyOverlappingCells( table, splitRow, startRow );
 
 	// Filter out cells that are not touching insides of the rectangular selection.
-	const cellsToSplit = overlappingCells.filter( ( { column, colspan } ) => isAffectedBySelection( column, colspan, limitColumns ) );
+	const cellsToSplit = overlappingCells.filter( ( { column, cellWidth } ) => isAffectedBySelection( column, cellWidth, limitColumns ) );
 
 	for ( const { cell } of cellsToSplit ) {
 		splitHorizontally( cell, splitRow, writer );
@@ -476,7 +464,7 @@ function doVerticalSplit( table, splitColumn, limitRows, writer ) {
 	const overlappingCells = getHorizontallyOverlappingCells( table, splitColumn );
 
 	// Filter out cells that are not touching insides of the rectangular selection.
-	const cellsToSplit = overlappingCells.filter( ( { row, rowspan } ) => isAffectedBySelection( row, rowspan, limitRows ) );
+	const cellsToSplit = overlappingCells.filter( ( { row, cellHeight } ) => isAffectedBySelection( row, cellHeight, limitRows ) );
 
 	for ( const { cell, column } of cellsToSplit ) {
 		splitVertically( cell, column, splitColumn, writer );
@@ -511,17 +499,13 @@ function isAffectedBySelection( index, span, limit ) {
 //  3 |   |   |   |   |
 //    +---+---+---+---+
 function adjustLastRowIndex( table, rowIndexes, columnIndexes ) {
-	const tableIterator = new TableWalker( table, {
-		startRow: rowIndexes.last,
-		endRow: rowIndexes.last
-	} );
-
-	const lastRowMap = Array.from( tableIterator ).filter( ( { column } ) => {
-		// Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785.
-		return columnIndexes.first <= column && column <= columnIndexes.last;
-	} );
+	const lastRowMap = Array.from( new TableWalker( table, {
+		startColumn: columnIndexes.first,
+		endColumn: columnIndexes.last,
+		row: rowIndexes.last
+	} ) );
 
-	const everyCellHasSingleRowspan = lastRowMap.every( ( { rowspan } ) => rowspan === 1 );
+	const everyCellHasSingleRowspan = lastRowMap.every( ( { cellHeight } ) => cellHeight === 1 );
 
 	// It is a "flat" row, so the last row index is OK.
 	if ( everyCellHasSingleRowspan ) {
@@ -529,7 +513,7 @@ function adjustLastRowIndex( table, rowIndexes, columnIndexes ) {
 	}
 
 	// Otherwise get any cell's rowspan and adjust the last row index.
-	const rowspanAdjustment = lastRowMap[ 0 ].rowspan - 1;
+	const rowspanAdjustment = lastRowMap[ 0 ].cellHeight - 1;
 	return rowIndexes.last + rowspanAdjustment;
 }
 
@@ -557,7 +541,7 @@ function adjustLastColumnIndex( table, rowIndexes, columnIndexes ) {
 		column: columnIndexes.last
 	} ) );
 
-	const everyCellHasSingleColspan = lastColumnMap.every( ( { colspan } ) => colspan === 1 );
+	const everyCellHasSingleColspan = lastColumnMap.every( ( { cellWidth } ) => cellWidth === 1 );
 
 	// It is a "flat" column, so the last column index is OK.
 	if ( everyCellHasSingleColspan ) {
@@ -565,6 +549,6 @@ function adjustLastColumnIndex( table, rowIndexes, columnIndexes ) {
 	}
 
 	// Otherwise get any cell's colspan and adjust the last column index.
-	const colspanAdjustment = lastColumnMap[ 0 ].colspan - 1;
+	const colspanAdjustment = lastColumnMap[ 0 ].cellWidth - 1;
 	return columnIndexes.last + colspanAdjustment;
 }

+ 3 - 3
packages/ckeditor5-table/src/tablenavigation.js

@@ -450,7 +450,7 @@ export default class TableNavigation extends Plugin {
 		const model = this.editor.model;
 
 		const table = findAncestor( 'table', focusCell );
-		const tableMap = [ ...new TableWalker( table, { includeSpanned: true } ) ];
+		const tableMap = [ ...new TableWalker( table, { includeAllSlots: true } ) ];
 		const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];
 
 		const currentCellInfo = tableMap.find( ( { cell } ) => cell == focusCell );
@@ -466,11 +466,11 @@ export default class TableNavigation extends Plugin {
 				break;
 
 			case 'right':
-				column += currentCellInfo.colspan;
+				column += currentCellInfo.cellWidth;
 				break;
 
 			case 'down':
-				row += currentCellInfo.rowspan;
+				row += currentCellInfo.cellHeight;
 				break;
 		}
 

+ 10 - 5
packages/ckeditor5-table/src/tableselection.js

@@ -109,7 +109,7 @@ export default class TableSelection extends Plugin {
 				endColumn
 			};
 
-			const table = cropTableToDimensions( sourceTable, cropDimensions, writer, this.editor.plugins.get( 'TableUtils' ) );
+			const table = cropTableToDimensions( sourceTable, cropDimensions, writer );
 
 			writer.insert( table, documentFragment, 0 );
 
@@ -479,10 +479,15 @@ export default class TableSelection extends Plugin {
 		// 2-dimensional array of the selected cells to ease flipping the order of cells for backward selections.
 		const selectionMap = new Array( endRow - startRow + 1 ).fill( null ).map( () => [] );
 
-		for ( const cellInfo of new TableWalker( findAncestor( 'table', anchorCell ), { startRow, endRow } ) ) {
-			if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
-				selectionMap[ cellInfo.row - startRow ].push( cellInfo.cell );
-			}
+		const walkerOptions = {
+			startRow,
+			endRow,
+			startColumn,
+			endColumn
+		};
+
+		for ( const { row, cell } of new TableWalker( findAncestor( 'table', anchorCell ), walkerOptions ) ) {
+			selectionMap[ row - startRow ].push( cell );
 		}
 
 		const flipVertically = endLocation.row < startLocation.row;

+ 6 - 16
packages/ckeditor5-table/src/tableselection/croptable.js

@@ -20,7 +20,7 @@ import TableWalker from '../tablewalker';
  *			endRow: 1,
  *			startColumn: 3,
  *			endColumn: 3
- *		}, tableUtils, writer );
+ *		}, writer );
  *
  * Calling the code above for the table below:
  *
@@ -44,10 +44,9 @@ import TableWalker from '../tablewalker';
  * @param {Number} cropDimensions.endRow
  * @param {Number} cropDimensions.endColumn
  * @param {module:engine/model/writer~Writer} writer
- * @param {module:table/tableutils~TableUtils} tableUtils
  * @returns {module:engine/model/element~Element}
  */
-export function cropTableToDimensions( sourceTable, cropDimensions, writer, tableUtils ) {
+export function cropTableToDimensions( sourceTable, cropDimensions, writer ) {
 	const { startRow, startColumn, endRow, endColumn } = cropDimensions;
 
 	// Create empty table with empty rows equal to crop height.
@@ -58,28 +57,19 @@ export function cropTableToDimensions( sourceTable, cropDimensions, writer, tabl
 		writer.insertElement( 'tableRow', croppedTable, 'end' );
 	}
 
-	const tableMap = [ ...new TableWalker( sourceTable, { startRow, endRow, includeSpanned: true } ) ];
+	const tableMap = [ ...new TableWalker( sourceTable, { startRow, endRow, startColumn, endColumn, includeAllSlots: true } ) ];
 
 	// Iterate over source table slots (including empty - spanned - ones).
-	for ( const { row: sourceRow, column: sourceColumn, cell: tableCell, isSpanned } of tableMap ) {
-		// Skip slots outside the cropped area.
-		// Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785.
-		if ( sourceColumn < startColumn || sourceColumn > endColumn ) {
-			continue;
-		}
-
+	for ( const { row: sourceRow, column: sourceColumn, cell: tableCell, isAnchor, cellAnchorRow, cellAnchorColumn } of tableMap ) {
 		// Row index in cropped table.
 		const rowInCroppedTable = sourceRow - startRow;
 		const row = croppedTable.getChild( rowInCroppedTable );
 
 		// For empty slots: fill the gap with empty table cell.
-		if ( isSpanned ) {
-			// TODO: Remove table utils usage. See: https://github.com/ckeditor/ckeditor5/issues/6785.
-			const { row: anchorRow, column: anchorColumn } = tableUtils.getCellLocation( tableCell );
-
+		if ( !isAnchor ) {
 			// But fill the gap only if the spanning cell is anchored outside cropped area.
 			// In the table from method jsdoc those cells are: "c" & "f".
-			if ( anchorRow < startRow || anchorColumn < startColumn ) {
+			if ( cellAnchorRow < startRow || cellAnchorColumn < startColumn ) {
 				createEmptyTableCell( writer, writer.createPositionAt( row, 'end' ) );
 			}
 		}

+ 43 - 50
packages/ckeditor5-table/src/tableutils.js

@@ -58,7 +58,7 @@ export default class TableUtils extends Plugin {
 
 		const rowIndex = table.getChildIndex( tableRow );
 
-		const tableWalker = new TableWalker( table, { startRow: rowIndex, endRow: rowIndex } );
+		const tableWalker = new TableWalker( table, { row: rowIndex } );
 
 		for ( const { cell, row, column } of tableWalker ) {
 			if ( cell === tableCell ) {
@@ -152,8 +152,8 @@ export default class TableUtils extends Plugin {
 			// Store spans of the reference row to reproduce it's structure. This array is column number indexed.
 			const rowColSpansMap = new Array( columns ).fill( 1 );
 
-			for ( const { row, column, rowspan, colspan, cell } of tableIterator ) {
-				const lastCellRow = row + rowspan - 1;
+			for ( const { row, column, cellHeight, cellWidth, cell } of tableIterator ) {
+				const lastCellRow = row + cellHeight - 1;
 
 				const isOverlappingInsertedRow = row < insertAt && insertAt <= lastCellRow;
 				const isReferenceRow = row <= copyStructureFrom && copyStructureFrom <= lastCellRow;
@@ -161,14 +161,14 @@ export default class TableUtils extends Plugin {
 				// If the cell is row-spanned and overlaps the inserted row, then reserve space for it in the row map.
 				if ( isOverlappingInsertedRow ) {
 					// This cell overlaps the inserted rows so we need to expand it further.
-					writer.setAttribute( 'rowspan', rowspan + rowsToInsert, cell );
+					writer.setAttribute( 'rowspan', cellHeight + rowsToInsert, cell );
 
 					// Mark this cell with negative number to indicate how many cells should be skipped when adding the new cells.
-					rowColSpansMap[ column ] = -colspan;
+					rowColSpansMap[ column ] = -cellWidth;
 				}
 				// Store the colspan from reference row.
 				else if ( isCopyStructure && isReferenceRow ) {
-					rowColSpansMap[ column ] = colspan;
+					rowColSpansMap[ column ] = cellWidth;
 				}
 			}
 
@@ -244,37 +244,31 @@ export default class TableUtils extends Plugin {
 				return;
 			}
 
-			const tableWalker = new TableWalker( table, { column: insertAt, includeSpanned: true } );
+			const tableWalker = new TableWalker( table, { column: insertAt, includeAllSlots: true } );
+
+			for ( const tableSlot of tableWalker ) {
+				const { row, cell, cellAnchorColumn, cellAnchorRow, cellWidth, cellHeight } = tableSlot;
 
-			for ( const { row, cell, cellIndex } of tableWalker ) {
 				// When iterating over column the table walker outputs either:
 				// - cells at given column index (cell "e" from method docs),
-				// - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeSpanned: true`),
+				// - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeAllSlots: true`),
 				// - or a cell from the same row which spans over this column (cell "a").
 
-				const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );
-				const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
-
-				if ( cellIndex !== insertAt && colspan > 1 ) {
-					// If column is different than `insertAt`, it is a cell that spans over an inserted column (cell "a" & "i").
+				if ( cellAnchorColumn < insertAt ) {
+					// If cell is anchored in previous column, it is a cell that spans over an inserted column (cell "a" & "i").
 					// For such cells expand them by a number of columns inserted.
-					writer.setAttribute( 'colspan', colspan + columnsToInsert, cell );
+					writer.setAttribute( 'colspan', cellWidth + columnsToInsert, cell );
 
-					// The `includeSpanned` option will output the "empty"/spanned column so skip this row already.
-					tableWalker.skipRow( row );
+					// This cell will overlap cells in rows below so skip them (because of `includeAllSlots` option) - (cell "a")
+					const lastCellRow = cellAnchorRow + cellHeight - 1;
 
-					// This cell will overlap cells in rows below so skip them also (because of `includeSpanned` option) - (cell "a")
-					if ( rowspan > 1 ) {
-						for ( let i = row + 1; i < row + rowspan; i++ ) {
-							tableWalker.skipRow( i );
-						}
+					for ( let i = row; i <= lastCellRow; i++ ) {
+						tableWalker.skipRow( i );
 					}
 				} else {
-					// It's either cell at this column index or spanned cell by a rowspanned cell from row above.
+					// It's either cell at this column index or spanned cell by a row-spanned cell from row above.
 					// In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
-					const insertPosition = writer.createPositionAt( table.getChild( row ), cellIndex );
-
-					createCells( columnsToInsert, writer, insertPosition );
+					createCells( columnsToInsert, writer, tableSlot.getPositionBefore() );
 				}
 			}
 		} );
@@ -388,10 +382,10 @@ export default class TableUtils extends Plugin {
 			const emptyRowsIndexes = [];
 
 			for ( let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex-- ) {
-				for ( const { cell, column, colspan } of [ ...new TableWalker( table ) ] ) {
+				for ( const { cell, column, cellWidth } of [ ...new TableWalker( table ) ] ) {
 					// If colspaned cell overlaps removed column decrease its span.
-					if ( column <= removedColumnIndex && colspan > 1 && column + colspan > removedColumnIndex ) {
-						updateNumericAttribute( 'colspan', colspan - 1, cell, writer );
+					if ( column <= removedColumnIndex && cellWidth > 1 && column + cellWidth > removedColumnIndex ) {
+						updateNumericAttribute( 'colspan', cellWidth - 1, cell, writer );
 					} else if ( column === removedColumnIndex ) {
 						const cellRow = cell.parent;
 
@@ -499,16 +493,16 @@ export default class TableUtils extends Plugin {
 				const { column: splitCellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );
 
 				// Find cells which needs to be expanded vertically - those on the same column or those that spans over split cell's column.
-				const cellsToUpdate = tableMap.filter( ( { cell, colspan, column } ) => {
+				const cellsToUpdate = tableMap.filter( ( { cell, cellWidth, column } ) => {
 					const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
-					const spansOverColumn = ( column < splitCellColumn && column + colspan > splitCellColumn );
+					const spansOverColumn = ( column < splitCellColumn && column + cellWidth > splitCellColumn );
 
 					return isOnSameColumn || spansOverColumn;
 				} );
 
 				// Expand cells vertically.
-				for ( const { cell, colspan } of cellsToUpdate ) {
-					writer.setAttribute( 'colspan', colspan + cellsToInsert, cell );
+				for ( const { cell, cellWidth } of cellsToUpdate ) {
+					writer.setAttribute( 'colspan', cellWidth + cellsToInsert, cell );
 				}
 
 				// Second step: create columns after split cell.
@@ -608,7 +602,7 @@ export default class TableUtils extends Plugin {
 				const tableMap = [ ...new TableWalker( table, {
 					startRow: splitCellRow,
 					endRow: splitCellRow + rowspan - 1,
-					includeSpanned: true
+					includeAllSlots: true
 				} ) ];
 
 				// Get spans of new (inserted) cells and span to update of split cell.
@@ -631,7 +625,9 @@ export default class TableUtils extends Plugin {
 					newCellsAttributes.colspan = colspan;
 				}
 
-				for ( const { column, row, cellIndex } of tableMap ) {
+				for ( const tableSlot of tableMap ) {
+					const { column, row } = tableSlot;
+
 					// As both newly created cells and the split cell might have rowspan,
 					// the insertion of new cells must go to appropriate rows:
 					//
@@ -643,9 +639,7 @@ export default class TableUtils extends Plugin {
 					const isInEvenlySplitRow = ( row + splitCellRow + updatedSpan ) % newCellsSpan === 0;
 
 					if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) {
-						const position = writer.createPositionAt( table.getChild( row ), cellIndex );
-
-						createCells( 1, writer, position, newCellsAttributes );
+						createCells( 1, writer, tableSlot.getPositionBefore(), newCellsAttributes );
 					}
 				}
 			}
@@ -659,12 +653,12 @@ export default class TableUtils extends Plugin {
 				const tableMap = [ ...new TableWalker( table, { startRow: 0, endRow: splitCellRow } ) ];
 
 				// First step: expand cells.
-				for ( const { cell, rowspan, row } of tableMap ) {
+				for ( const { cell, cellHeight, row } of tableMap ) {
 					// Expand rowspan of cells that are either:
 					// - on the same row as current cell,
 					// - or are below split cell row and overlaps that row.
-					if ( cell !== tableCell && row + rowspan > splitCellRow ) {
-						const rowspanToSet = rowspan + cellsToInsert;
+					if ( cell !== tableCell && row + cellHeight > splitCellRow ) {
+						const rowspanToSet = cellHeight + cellsToInsert;
 
 						writer.setAttribute( 'rowspan', rowspanToSet, cell );
 					}
@@ -829,14 +823,14 @@ 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;
+	for ( const { row, column, cellHeight, cell } of new TableWalker( table, { endRow: last } ) ) {
+		const lastRowOfCell = row + cellHeight - 1;
 
 		const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;
 
 		if ( isCellStickingOutFromRemovedRows ) {
 			const rowspanInRemovedSection = last - row + 1;
-			const rowSpanToSet = rowspan - rowspanInRemovedSection;
+			const rowSpanToSet = cellHeight - rowspanInRemovedSection;
 
 			cellsToMove.set( column, {
 				cell,
@@ -860,7 +854,7 @@ function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
 
 			cellsToTrim.push( {
 				cell,
-				rowspan: rowspan - rowspanAdjustment
+				rowspan: cellHeight - rowspanAdjustment
 			} );
 		}
 	}
@@ -869,9 +863,8 @@ function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
 
 function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
 	const tableWalker = new TableWalker( table, {
-		includeSpanned: true,
-		startRow: targetRowIndex,
-		endRow: targetRowIndex
+		includeAllSlots: true,
+		row: targetRowIndex
 	} );
 
 	const tableRowMap = [ ...tableWalker ];
@@ -879,7 +872,7 @@ function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
 
 	let previousCell;
 
-	for ( const { column, cell, isSpanned } of tableRowMap ) {
+	for ( const { column, cell, isAnchor } of tableRowMap ) {
 		if ( cellsToMove.has( column ) ) {
 			const { cell: cellToMove, rowspan } = cellsToMove.get( column );
 
@@ -891,7 +884,7 @@ function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
 			updateNumericAttribute( 'rowspan', rowspan, cellToMove, writer );
 
 			previousCell = cellToMove;
-		} else if ( !isSpanned ) {
+		} else if ( isAnchor ) {
 			// If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
 			previousCell = cell;
 		}

+ 256 - 132
packages/ckeditor5-table/src/tablewalker.js

@@ -7,9 +7,11 @@
  * @module table/tablewalker
  */
 
+// @if CK_DEBUG // import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
 /**
  * The table iterator class. It allows to iterate over table cells. For each cell the iterator yields
- * {@link module:table/tablewalker~TableWalkerValue} with proper table cell attributes.
+ * {@link module:table/tablewalker~TableSlot} with proper table cell attributes.
  */
 export default class TableWalker {
 	/**
@@ -18,18 +20,18 @@ export default class TableWalker {
 	 * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
 	 * It walks row by row and column by column in order to output values defined in the constructor.
 	 * By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
-	 * pass the `includeSpanned` option to the constructor.
+	 * pass the `includeAllSlots` option to the constructor.
 	 *
 	 * The most important values of the iterator are column and row indexes of a cell.
 	 *
-	 * See {@link module:table/tablewalker~TableWalkerValue} what values are returned by the table walker.
+	 * See {@link module:table/tablewalker~TableSlot} what values are returned by the table walker.
 	 *
 	 * To iterate over a given row:
 	 *
 	 *		const tableWalker = new TableWalker( table, { startRow: 1, endRow: 2 } );
 	 *
-	 *		for ( const cellInfo of tableWalker ) {
-	 *			console.log( 'A cell at row ' + cellInfo.row + ' and column ' + cellInfo.column );
+	 *		for ( const tableSlot of tableWalker ) {
+	 *			console.log( 'A cell at row', tableSlot.row, 'and column', tableSlot.column );
 	 *		}
 	 *
 	 * For instance the code above for the following table:
@@ -53,28 +55,37 @@ export default class TableWalker {
 	 *
 	 * To also iterate over spanned cells:
 	 *
-	 *		const tableWalker = new TableWalker( table, { startRow: 1, endRow: 1, includeSpanned: true } );
+	 *		const tableWalker = new TableWalker( table, { row: 1, includeAllSlots: true } );
 	 *
-	 *		for ( const value of tableWalker ) {
-	 *			console.log( 'Cell at ' + value.row + ' x ' + value.column + ' : ' + ( value.isSpanned ? 'is spanned' : 'has data' ) );
+	 *		for ( const tableSlot of tableWalker ) {
+	 *			console.log( 'Slot at', tableSlot.row, 'x', tableSlot.column, ':', tableSlot.isAnchor ? 'is anchored' : 'is spanned' );
 	 *		}
 	 *
 	 * will log in the console for the table from the previous example:
 	 *
 	 *		'Cell at 1 x 0 : is spanned'
 	 *		'Cell at 1 x 1 : is spanned'
-	 *		'Cell at 1 x 2 : has data'
+	 *		'Cell at 1 x 2 : is anchored'
 	 *		'Cell at 1 x 3 : is spanned'
-	 *		'Cell at 1 x 4 : has data'
-	 *		'Cell at 1 x 5 : has data'
+	 *		'Cell at 1 x 4 : is anchored'
+	 *		'Cell at 1 x 5 : is anchored'
+	 *
+	 * **Note**: Option `row` is a shortcut that sets both `startRow` and `endRow` to the same row.
+	 * (Use either `row` or `startRow` and `endRow` but never together). Similarly the `column` option sets both `startColumn`
+	 * and `endColumn` to the same column (Use either `column` or `startColumn` and `endColumn` but never together).
 	 *
 	 * @constructor
 	 * @param {module:engine/model/element~Element} table A table over which the walker iterates.
 	 * @param {Object} [options={}] An object with configuration.
+	 * @param {Number} [options.row] A row index for which this iterator will output cells.
+	 * Can't be used together with `startRow` and `endRow`.
+	 * @param {Number} [options.startRow=0] A row index from which this iterator should start. Can't be used together with `row`.
+	 * @param {Number} [options.endRow] A row index at which this iterator should end. Can't be used together with `row`.
 	 * @param {Number} [options.column] A column index for which this iterator will output cells.
-	 * @param {Number} [options.startRow=0] A row index from which this iterator should start.
-	 * @param {Number} [options.endRow] A row index at which this iterator should end.
-	 * @param {Boolean} [options.includeSpanned=false] Also return values for spanned cells.
+	 * Can't be used together with `startColumn` and `endColumn`.
+	 * @param {Number} [options.startColumn=0] A column index from which this iterator should start. Can't be used together with `column`.
+	 * @param {Number} [options.endColumn] A column index at which this iterator should end. Can't be used together with `column`.
+	 * @param {Boolean} [options.includeAllSlots=false] Also return values for spanned cells.
 	 */
 	constructor( table, options = {} ) {
 		/**
@@ -82,40 +93,54 @@ export default class TableWalker {
 		 *
 		 * @readonly
 		 * @member {module:engine/model/element~Element}
+		 * @protected
 		 */
-		this.table = table;
+		this._table = table;
 
 		/**
 		 * A row index from which this iterator will start.
 		 *
 		 * @readonly
 		 * @member {Number}
+		 * @private
 		 */
-		this.startRow = options.startRow || 0;
+		this._startRow = options.row !== undefined ? options.row : options.startRow || 0;
 
 		/**
 		 * A row index at which this iterator will end.
 		 *
 		 * @readonly
 		 * @member {Number}
+		 * @private
 		 */
-		this.endRow = typeof options.endRow == 'number' ? options.endRow : undefined;
+		this._endRow = options.row !== undefined ? options.row : options.endRow;
 
 		/**
-		 * Enables output of spanned cells that are normally not yielded.
+		 * If set, the table walker will only output cells from a given column and following ones or cells that overlap them.
 		 *
 		 * @readonly
-		 * @member {Boolean}
+		 * @member {Number}
+		 * @private
 		 */
-		this.includeSpanned = !!options.includeSpanned;
+		this._startColumn = options.column !== undefined ? options.column : options.startColumn || 0;
 
 		/**
-		 * If set, the table walker will only output cells of a given column or cells that overlap it.
+		 * If set, the table walker will only output cells up to a given column.
 		 *
 		 * @readonly
 		 * @member {Number}
+		 * @private
 		 */
-		this.column = typeof options.column == 'number' ? options.column : undefined;
+		this._endColumn = options.column !== undefined ? options.column : options.endColumn;
+
+		/**
+		 * Enables output of spanned cells that are normally not yielded.
+		 *
+		 * @readonly
+		 * @member {Boolean}
+		 * @private
+		 */
+		this._includeAllSlots = !!options.includeAllSlots;
 
 		/**
 		 * Row indexes to skip from the iteration.
@@ -129,28 +154,25 @@ export default class TableWalker {
 		/**
 		 * The current row index.
 		 *
-		 * @readonly
 		 * @member {Number}
-		 * @private
+		 * @protected
 		 */
 		this._row = 0;
 
 		/**
 		 * The current column index.
 		 *
-		 * @readonly
 		 * @member {Number}
-		 * @private
+		 * @protected
 		 */
 		this._column = 0;
 
 		/**
-		 * The cell index in a parent row. For spanned cells when {@link #includeSpanned} is set to `true`,
+		 * The cell index in a parent row. For spanned cells when {@link #_includeAllSlots} is set to `true`,
 		 * this represents the index of the next table cell.
 		 *
-		 * @readonly
 		 * @member {Number}
-		 * @private
+		 * @protected
 		 */
 		this._cellIndex = 0;
 
@@ -158,18 +180,24 @@ export default class TableWalker {
 		 * Holds a map of spanned cells in a table.
 		 *
 		 * @readonly
-		 * @member {Map<Number, Map.<Number, module:engine/model/element~Element>>}
+		 * @member {Map.<Number, Map.<Number, Object>>}
 		 * @private
 		 */
 		this._spannedCells = new Map();
 
+		/**
+		 * Index of the next column where a cell is anchored.
+		 *
+		 * @member {Number}
+		 * @private
+		 */
 		this._nextCellAtColumn = -1;
 	}
 
 	/**
 	 * Iterable interface.
 	 *
-	 * @returns {Iterable.<module:table/tablewalker~TableWalkerValue>}
+	 * @returns {Iterable.<module:table/tablewalker~TableSlot>}
 	 */
 	[ Symbol.iterator ]() {
 		return this;
@@ -178,34 +206,34 @@ export default class TableWalker {
 	/**
 	 * Gets the next table walker's value.
 	 *
-	 * @returns {module:table/tablewalker~TableWalkerValue} The next table walker's value.
+	 * @returns {module:table/tablewalker~TableSlot} The next table walker's value.
 	 */
 	next() {
-		const row = this.table.getChild( this._row );
+		const row = this._table.getChild( this._row );
 
 		// Iterator is done when there's no row (table ended) or the row is after `endRow` limit.
 		if ( !row || this._isOverEndRow() ) {
 			return { done: true };
 		}
 
-		let cell, skipCurrentValue, outValue;
+		if ( this._isOverEndColumn() ) {
+			return this._advanceToNextRow();
+		}
+
+		let outValue = null;
 
-		if ( this._isSpanned( this._row, this._column ) ) {
-			cell = this._getSpanned( this._row, this._column );
+		const spanData = this._getSpanned();
 
-			skipCurrentValue = !this.includeSpanned || this._shouldSkipRow() || this._shouldSkipColumn();
-			outValue = this._formatOutValue( cell, this._column, true );
+		if ( spanData ) {
+			if ( this._includeAllSlots && !this._shouldSkipSlot() ) {
+				outValue = this._formatOutValue( spanData.cell, spanData.row, spanData.column );
+			}
 		} else {
-			cell = row.getChild( this._cellIndex );
+			const cell = row.getChild( this._cellIndex );
 
 			if ( !cell ) {
 				// If there are no more cells left in row advance to the next row.
-				this._row++;
-				this._column = 0;
-				this._cellIndex = 0;
-				this._nextCellAtColumn = -1;
-
-				return this.next();
+				return this._advanceToNextRow();
 			}
 
 			const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
@@ -213,13 +241,14 @@ export default class TableWalker {
 
 			// Record this cell spans if it's not 1x1 cell.
 			if ( colspan > 1 || rowspan > 1 ) {
-				this._recordSpans( this._row, this._column, rowspan, colspan, cell );
+				this._recordSpans( cell, rowspan, colspan );
 			}
 
-			this._nextCellAtColumn = this._column + colspan;
+			if ( !this._shouldSkipSlot() ) {
+				outValue = this._formatOutValue( cell );
+			}
 
-			skipCurrentValue = this._shouldSkipRow() || this._shouldSkipColumn();
-			outValue = this._formatOutValue( cell, this._column, false, rowspan, colspan );
+			this._nextCellAtColumn = this._column + colspan;
 		}
 
 		// Advance to the next column before returning value.
@@ -230,7 +259,7 @@ export default class TableWalker {
 		}
 
 		// The current value will be returned only if current row and column are not skipped.
-		return skipCurrentValue ? this.next() : outValue;
+		return outValue || this.next();
 	}
 
 	/**
@@ -244,122 +273,112 @@ export default class TableWalker {
 	}
 
 	/**
-	 * Checks if the current row is over {@link #endRow}.
+	 * Advances internal cursor to the next row.
 	 *
 	 * @private
-	 * @returns {Boolean}
+	 * @returns {module:table/tablewalker~TableSlot}
 	 */
-	_isOverEndRow() {
-		// If {@link #endRow) is defined skip all rows above it.
-		return this.endRow !== undefined && this._row > this.endRow;
+	_advanceToNextRow() {
+		this._row++;
+		this._column = 0;
+		this._cellIndex = 0;
+		this._nextCellAtColumn = -1;
+
+		return this.next();
 	}
 
 	/**
-	 * A common method for formatting the iterator's output value.
+	 * Checks if the current row is over {@link #_endRow}.
 	 *
 	 * @private
-	 * @param {module:engine/model/element~Element} cell The table cell to output.
-	 * @param {Number} column The column index (use the cached value).
-	 * @param {Boolean} isSpanned Whether the value is returned for a spanned cell location or an actual cell.
-	 * @param {Number} rowspan The rowspan of the current cell.
-	 * @param {Number} colspan The colspan of the current cell.
-	 * @returns {{done: Boolean, value: {cell: *, row: Number, column: *, rowspan: *, colspan: *, cellIndex: Number}}}
+	 * @returns {Boolean}
 	 */
-	_formatOutValue( cell, column, isSpanned, rowspan = 1, colspan = 1 ) {
-		return {
-			done: false,
-			value: {
-				cell,
-				row: this._row,
-				column,
-				isSpanned,
-				rowspan,
-				colspan,
-				cellIndex: this._cellIndex
-			}
-		};
+	_isOverEndRow() {
+		// If #_endRow is defined skip all rows after it.
+		return this._endRow !== undefined && this._row > this._endRow;
 	}
 
 	/**
-	 * Checks if the current row should be skipped.
+	 * Checks if the current cell is over {@link #_endColumn}
 	 *
 	 * @private
 	 * @returns {Boolean}
 	 */
-	_shouldSkipRow() {
-		const rowIsBelowStartRow = this._row < this.startRow;
-		const rowIsMarkedAsSkipped = this._skipRows.has( this._row );
-
-		return rowIsBelowStartRow || rowIsMarkedAsSkipped;
+	_isOverEndColumn() {
+		// If #_endColumn is defined skip all cells after it.
+		return this._endColumn !== undefined && this._column > this._endColumn;
 	}
 
 	/**
-	 * Checks if the current column should be skipped.
+	 * A common method for formatting the iterator's output value.
 	 *
 	 * @private
-	 * @returns {Boolean}
+	 * @param {module:engine/model/element~Element} cell The table cell to output.
+	 * @param {Number} [anchorRow] The row index of a cell anchor slot.
+	 * @param {Number} [anchorColumn] The column index of a cell anchor slot.
+	 * @returns {{done: Boolean, value: {cell: *, row: Number, column: *, rowspan: *, colspan: *, cellIndex: Number}}}
 	 */
-	_shouldSkipColumn() {
-		if ( this.column === undefined ) {
-			// The {@link #column} is not defined so output all columns.
-			return false;
-		}
-
-		return this.column != this._column;
+	_formatOutValue( cell, anchorRow = this._row, anchorColumn = this._column ) {
+		return {
+			done: false,
+			value: new TableSlot( this, cell, anchorRow, anchorColumn )
+		};
 	}
 
 	/**
-	 * Checks if the current cell location (row x column) is spanned by another cell.
+	 * Checks if the current slot should be skipped.
 	 *
 	 * @private
-	 * @param {Number} row The row index of a cell location to check.
-	 * @param {Number} column The column index of a cell location to check.
 	 * @returns {Boolean}
 	 */
-	_isSpanned( row, column ) {
-		if ( !this._spannedCells.has( row ) ) {
-			// No spans for given row.
-			return false;
-		}
+	_shouldSkipSlot() {
+		const rowIsMarkedAsSkipped = this._skipRows.has( this._row );
+		const rowIsBeforeStartRow = this._row < this._startRow;
 
-		const rowSpans = this._spannedCells.get( row );
+		const columnIsBeforeStartColumn = this._column < this._startColumn;
+		const columnIsAfterEndColumn = this._endColumn !== undefined && this._column > this._endColumn;
 
-		// If spans for given rows has entry for column it means that this location if spanned by other cell.
-		return rowSpans.has( column );
+		return rowIsMarkedAsSkipped || rowIsBeforeStartRow || columnIsBeforeStartColumn || columnIsAfterEndColumn;
 	}
 
 	/**
-	 * Returns the cell element that is spanned over the `row` x `column` location.
+	 * Returns the cell element that is spanned over the current cell location.
 	 *
 	 * @private
-	 * @param {Number} row The row index of the cell location.
-	 * @param {Number} column The column index of the cell location.
 	 * @returns {module:engine/model/element~Element}
 	 */
-	_getSpanned( row, column ) {
-		return this._spannedCells.get( row ).get( column );
+	_getSpanned() {
+		const rowMap = this._spannedCells.get( this._row );
+
+		// No spans for given row.
+		if ( !rowMap ) {
+			return null;
+		}
+
+		// If spans for given rows has entry for column it means that this location if spanned by other cell.
+		return rowMap.get( this._column ) || null;
 	}
 
 	/**
 	 * Updates spanned cells map relative to the current cell location and its span dimensions.
 	 *
 	 * @private
-	 * @param {Number} row The row index of a cell.
-	 * @param {Number} column The column index of a cell.
+	 * @param {module:engine/model/element~Element} cell A cell that is spanned.
 	 * @param {Number} rowspan Cell height.
 	 * @param {Number} colspan Cell width.
-	 * @param {module:engine/model/element~Element} cell A cell that is spanned.
 	 */
-	_recordSpans( row, column, rowspan, colspan, cell ) {
-		// This will update all cell locations after current column - ie a cell has colspan set.
-		for ( let columnToUpdate = column + 1; columnToUpdate <= column + colspan - 1; columnToUpdate++ ) {
-			this._markSpannedCell( row, columnToUpdate, cell );
-		}
+	_recordSpans( cell, rowspan, colspan ) {
+		const data = {
+			cell,
+			row: this._row,
+			column: this._column
+		};
 
-		// This will update all rows below current up to row's height.
-		for ( let rowToUpdate = row + 1; rowToUpdate < row + rowspan; rowToUpdate++ ) {
-			for ( let columnToUpdate = column; columnToUpdate <= column + colspan - 1; columnToUpdate++ ) {
-				this._markSpannedCell( rowToUpdate, columnToUpdate, cell );
+		for ( let rowToUpdate = this._row; rowToUpdate < this._row + rowspan; rowToUpdate++ ) {
+			for ( let columnToUpdate = this._column; columnToUpdate < this._column + colspan; columnToUpdate++ ) {
+				if ( rowToUpdate != this._row || columnToUpdate != this._column ) {
+					this._markSpannedCell( rowToUpdate, columnToUpdate, data );
+				}
 			}
 		}
 	}
@@ -370,30 +389,135 @@ export default class TableWalker {
 	 * @private
 	 * @param {Number} row The row index of the cell location.
 	 * @param {Number} column The column index of the cell location.
-	 * @param {module:engine/model/element~Element} cell A cell that is spanned.
+	 * @param {Object} data A spanned cell details (cell element, anchor row and column).
 	 */
-	_markSpannedCell( row, column, cell ) {
+	_markSpannedCell( row, column, data ) {
 		if ( !this._spannedCells.has( row ) ) {
 			this._spannedCells.set( row, new Map() );
 		}
 
 		const rowSpans = this._spannedCells.get( row );
 
-		rowSpans.set( column, cell );
+		rowSpans.set( column, data );
 	}
 }
 
 /**
  * An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
- *
- * @typedef {Object} module:table/tablewalker~TableWalkerValue
- * @property {module:engine/model/element~Element} cell The current table cell.
- * @property {Number} row The row index of a cell.
- * @property {Number} column The column index of a cell. Column index is adjusted to widths and heights of previous cells.
- * @param {Boolean} isSpanned Whether the value is returned for a spanned cell location or an actual cell.
- * @property {Number} colspan The `colspan` attribute of a cell. If the model attribute is not present, it is set to `1`. For spanned
- * table locations, it is set to `1`.
- * @property {Number} rowspan The `rowspan` attribute of a cell. If the model attribute is not present, it is set to `1`. For spanned
- * table locations, it is set to `1`.
- * @property {Number} cellIndex The index of the current cell in the parent row.
  */
+class TableSlot {
+	/**
+	 * Creates an instance of the table walker value.
+	 *
+	 * @protected
+	 * @param {module:table/tablewalker~TableWalker} tableWalker The table walker instance.
+	 * @param {module:engine/model/element~Element} cell The current table cell.
+	 * @param {Number} anchorRow The row index of a cell anchor slot.
+	 * @param {Number} anchorColumn The column index of a cell anchor slot.
+	 */
+	constructor( tableWalker, cell, anchorRow, anchorColumn ) {
+		/**
+		 * The current table cell.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/element~Element}
+		 */
+		this.cell = cell;
+
+		/**
+		 * The row index of a table slot.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.row = tableWalker._row;
+
+		/**
+		 * The column index of a table slot.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.column = tableWalker._column;
+
+		/**
+		 * The row index of a cell anchor slot.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.cellAnchorRow = anchorRow;
+
+		/**
+		 * The column index of a cell anchor slot.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.cellAnchorColumn = anchorColumn;
+
+		/**
+		 * The index of the current cell in the parent row.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 * @private
+		 */
+		this._cellIndex = tableWalker._cellIndex;
+
+		/**
+		 * The table element.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/element~Element}
+		 * @private
+		 */
+		this._table = tableWalker._table;
+	}
+
+	/**
+	 * Whether the cell is anchored in the current slot.
+	 *
+	 * @readonly
+	 * @returns {Boolean}
+	 */
+	get isAnchor() {
+		return this.row === this.cellAnchorRow && this.column === this.cellAnchorColumn;
+	}
+
+	/**
+	 * The widht of a cell defined by a `colspan` attribute. If the model attribute is not present, it is set to `1`.
+	 *
+	 * @readonly
+	 * @returns {Number}
+	 */
+	get cellWidth() {
+		return parseInt( this.cell.getAttribute( 'colspan' ) || 1 );
+	}
+
+	/**
+	 * The height of a cell defined by a `rowspan` attribute. If the model attribute is not present, it is set to `1`.
+	 *
+	 * @readonly
+	 * @returns {Number}
+	 */
+	get cellHeight() {
+		return parseInt( this.cell.getAttribute( 'rowspan' ) || 1 );
+	}
+
+	/**
+	 * Returns the {@link module:engine/model/position~Position} before the table slot.
+	 *
+	 * @returns {module:engine/model/position~Position}
+	 */
+	getPositionBefore() {
+		const model = this._table.root.document.model;
+
+		return model.createPositionAt( this._table.getChild( this.row ), this._cellIndex );
+	}
+
+	// @if CK_DEBUG // get isSpanned() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
+	// @if CK_DEBUG // get colspan() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
+	// @if CK_DEBUG // get rowspan() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
+	// @if CK_DEBUG // get cellIndex() { throw new CKEditorError( 'tablewalker-improper-api-usage', this ); }
+}

+ 11 - 12
packages/ckeditor5-table/src/utils.js

@@ -269,7 +269,7 @@ export function isSelectionRectangular( selectedTableCells, tableUtils ) {
  * @param {module:engine/model/element~Element} table The table to check.
  * @param {Number} overlapRow The index of the row to check.
  * @param {Number} [startRow=0] A row to start analysis. Use it when it is known that the cells above that row will not overlap.
- * @returns {Array.<module:table/tablewalker~TableWalkerValue>}
+ * @returns {Array.<module:table/tablewalker~TableSlot>}
  */
 export function getVerticallyOverlappingCells( table, overlapRow, startRow = 0 ) {
 	const cells = [];
@@ -277,8 +277,8 @@ export function getVerticallyOverlappingCells( table, overlapRow, startRow = 0 )
 	const tableWalker = new TableWalker( table, { startRow, endRow: overlapRow - 1 } );
 
 	for ( const slotInfo of tableWalker ) {
-		const { row, rowspan } = slotInfo;
-		const cellEndRow = row + rowspan - 1;
+		const { row, cellHeight } = slotInfo;
+		const cellEndRow = row + cellHeight - 1;
 
 		if ( row < overlapRow && overlapRow <= cellEndRow ) {
 			cells.push( slotInfo );
@@ -318,20 +318,19 @@ export function splitHorizontally( tableCell, splitRow, writer ) {
 
 	const startRow = rowIndex;
 	const endRow = startRow + newRowspan;
-	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeSpanned: true } ) ];
+	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeAllSlots: true } ) ];
 
 	let columnIndex;
 
-	for ( const { row, column, cell, cellIndex } of tableMap ) {
+	for ( const tableSlot of tableMap ) {
+		const { row, column, cell } = tableSlot;
+
 		if ( cell === tableCell && columnIndex === undefined ) {
 			columnIndex = column;
 		}
 
 		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
-			const tableRow = table.getChild( row );
-			const tableCellPosition = writer.createPositionAt( tableRow, cellIndex );
-
-			createEmptyTableCell( writer, tableCellPosition, newCellAttributes );
+			createEmptyTableCell( writer, tableSlot.getPositionBefore(), newCellAttributes );
 		}
 	}
 
@@ -363,7 +362,7 @@ export function splitHorizontally( tableCell, splitRow, writer ) {
  *
  * @param {module:engine/model/element~Element} table The table to check.
  * @param {Number} overlapColumn The index of the column to check.
- * @returns {Array.<module:table/tablewalker~TableWalkerValue>}
+ * @returns {Array.<module:table/tablewalker~TableSlot>}
  */
 export function getHorizontallyOverlappingCells( table, overlapColumn ) {
 	const cellsToSplit = [];
@@ -371,8 +370,8 @@ export function getHorizontallyOverlappingCells( table, overlapColumn ) {
 	const tableWalker = new TableWalker( table );
 
 	for ( const slotInfo of tableWalker ) {
-		const { column, colspan } = slotInfo;
-		const cellEndColumn = column + colspan - 1;
+		const { column, cellWidth } = slotInfo;
+		const cellEndColumn = column + cellWidth - 1;
 
 		if ( column < overlapColumn && overlapColumn <= cellEndColumn ) {
 			cellsToSplit.push( slotInfo );

+ 12 - 20
packages/ckeditor5-table/tests/_utils/utils.js

@@ -498,7 +498,7 @@ function getClassToSet( attributes ) {
  * @returns {String}
  */
 export function createTableAsciiArt( model, table ) {
-	const tableMap = [ ...new TableWalker( table, { includeSpanned: true } ) ];
+	const tableMap = [ ...new TableWalker( table, { includeAllSlots: true } ) ];
 
 	if ( !tableMap.length ) {
 		return '';
@@ -519,25 +519,17 @@ export function createTableAsciiArt( model, table ) {
 		for ( let column = 0; column <= lastColumn; column++ ) {
 			const cellInfo = tableMap[ row * columns + column ];
 
-			if ( cellInfo.rowspan > 1 || cellInfo.colspan > 1 ) {
-				for ( let subRow = row; subRow < row + cellInfo.rowspan; subRow++ ) {
-					for ( let subColumn = column; subColumn < column + cellInfo.colspan; subColumn++ ) {
-						const subCellInfo = tableMap[ subRow * columns + subColumn ];
+			const isColSpan = cellInfo.cellAnchorColumn != cellInfo.column;
+			const isRowSpan = cellInfo.cellAnchorRow != cellInfo.row;
 
-						subCellInfo.isColSpan = subColumn > column;
-						subCellInfo.isRowSpan = subRow > row;
-					}
-				}
-			}
-
-			gridLine += !cellInfo.isColSpan || !cellInfo.isRowSpan ? '+' : ' ';
-			gridLine += !cellInfo.isRowSpan ? '----' : '    ';
+			gridLine += !isColSpan || !isRowSpan ? '+' : ' ';
+			gridLine += !isRowSpan ? '----' : '    ';
 
 			let contents = getElementPlainText( model, cellInfo.cell ).substring( 0, 2 );
 			contents += ' '.repeat( 2 - contents.length );
 
-			contentLine += !cellInfo.isColSpan ? '|' : ' ';
-			contentLine += !cellInfo.isColSpan && !cellInfo.isRowSpan ? ` ${ contents } ` : '    ';
+			contentLine += !isColSpan ? '|' : ' ';
+			contentLine += !isColSpan && !isRowSpan ? ` ${ contents } ` : '    ';
 
 			if ( column == lastColumn ) {
 				gridLine += '+';
@@ -578,23 +570,23 @@ export function prepareModelTableInput( model, table ) {
 	const result = [];
 	let row = [];
 
-	for ( const cellInfo of new TableWalker( table, { includeSpanned: true } ) ) {
+	for ( const cellInfo of new TableWalker( table, { includeAllSlots: true } ) ) {
 		if ( cellInfo.column == 0 && cellInfo.row > 0 ) {
 			result.push( row );
 			row = [];
 		}
 
-		if ( cellInfo.isSpanned ) {
+		if ( !cellInfo.isAnchor ) {
 			continue;
 		}
 
 		const contents = getElementPlainText( model, cellInfo.cell );
 
-		if ( cellInfo.colspan > 1 || cellInfo.rowspan > 1 ) {
+		if ( cellInfo.cellWidth > 1 || cellInfo.cellHeight > 1 ) {
 			row.push( {
 				contents,
-				...( cellInfo.colspan > 1 ? { colspan: cellInfo.colspan } : null ),
-				...( cellInfo.rowspan > 1 ? { rowspan: cellInfo.rowspan } : null )
+				...( cellInfo.cellWidth > 1 ? { colspan: cellInfo.cellWidth } : null ),
+				...( cellInfo.cellHeight > 1 ? { rowspan: cellInfo.cellHeight } : null )
 			} );
 		} else {
 			row.push( contents );

+ 1 - 1
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -1421,7 +1421,7 @@ describe( 'table clipboard', () => {
 					] );
 				} );
 
-				it( 'handles pasting table that has cell with colspan (multiple ending rows in the selection are spanned)', () => {
+				it( 'handles pasting table that has cell with rowspan (multiple ending rows in the selection are spanned)', () => {
 					// +----+----+----+
 					// | 00 | 01 | 02 |
 					// +----+    +    +

+ 7 - 0
packages/ckeditor5-table/tests/tableutils.js

@@ -481,6 +481,13 @@ describe( 'TableUtils', () => {
 		} );
 
 		it( 'should skip row & column spanned cells', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +----+----+----+
+			// | 20 | 21 | 22 |
+			// +----+----+----+
 			setData( model, modelTable( [
 				[ { colspan: 2, rowspan: 2, contents: '00[]' }, '02' ],
 				[ '12' ],

+ 474 - 98
packages/ckeditor5-table/tests/tablewalker.js

@@ -5,6 +5,7 @@
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import { defaultConversion, defaultSchema, modelTable } from './_utils/utils';
 
@@ -26,237 +27,612 @@ describe( 'TableWalker', () => {
 			} );
 	} );
 
-	function testWalker( tableData, expected, options ) {
+	function testWalker( tableData, expected, options, skip ) {
 		setData( model, modelTable( tableData ) );
 
-		const iterator = new TableWalker( root.getChild( 0 ), options );
+		const walker = new TableWalker( root.getChild( 0 ), options );
 
-		const result = [];
-
-		for ( const tableInfo of iterator ) {
-			result.push( tableInfo );
+		if ( skip !== undefined ) {
+			walker.skipRow( skip );
 		}
 
-		const formattedResult = result.map( ( { row, column, isSpanned, cell, cellIndex } ) => {
-			const result = {
+		const result = [ ...walker ];
+
+		const formattedResult = result.map( tableSlot => {
+			const { cell, row, column, isAnchor, cellWidth, cellHeight, cellAnchorRow, cellAnchorColumn } = tableSlot;
+
+			return {
 				row,
 				column,
 				data: cell && cell.getChild( 0 ).getChild( 0 ).data,
-				index: cellIndex
+				index: tableSlot.getPositionBefore().offset,
+				...( cellAnchorRow != row ? { anchorRow: cellAnchorRow } : null ),
+				...( cellAnchorColumn != column ? { anchorColumn: cellAnchorColumn } : null ),
+				...( isAnchor ? { isAnchor } : null ),
+				...( cellWidth > 1 ? { width: cellWidth } : null ),
+				...( cellHeight > 1 ? { height: cellHeight } : null )
 			};
-
-			if ( isSpanned ) {
-				result.isSpanned = true;
-			}
-
-			return result;
 		} );
 
 		expect( formattedResult ).to.deep.equal( expected );
 	}
 
 	it( 'should iterate over a table', () => {
+		// +----+----+
+		// | 00 | 01 |
+		// +----+----+
+		// | 10 | 11 |
+		// +----+----+
 		testWalker( [
 			[ '00', '01' ],
 			[ '10', '11' ]
 		], [
-			{ row: 0, column: 0, index: 0, data: '00' },
-			{ row: 0, column: 1, index: 1, data: '01' },
-			{ row: 1, column: 0, index: 0, data: '10' },
-			{ row: 1, column: 1, index: 1, data: '11' }
+			{ row: 0, column: 0, index: 0, data: '00', isAnchor: true },
+			{ row: 0, column: 1, index: 1, data: '01', isAnchor: true },
+			{ row: 1, column: 0, index: 0, data: '10', isAnchor: true },
+			{ row: 1, column: 1, index: 1, data: '11', isAnchor: true }
 		] );
 	} );
 
-	it( 'should properly output column indexes of a table that has colspans', () => {
+	it( 'should properly output column indexes of a table that has col-spans', () => {
+		// +----+----+----+
+		// | 00      | 13 |
+		// +----+----+----+
 		testWalker( [
 			[ { colspan: 2, contents: '00' }, '13' ]
 		], [
-			{ row: 0, column: 0, index: 0, data: '00' },
-			{ row: 0, column: 2, index: 1, data: '13' }
+			{ row: 0, column: 0, index: 0, data: '00', isAnchor: true, width: 2 },
+			{ row: 0, column: 2, index: 1, data: '13', isAnchor: true }
 		] );
 	} );
 
-	it( 'should properly output column indexes of a table that has rowspans', () => {
+	it( 'should properly output column indexes of a table that has row-spans', () => {
+		// +----+----+----+
+		// | 00      | 02 |
+		// +         +----+
+		// |         | 12 |
+		// +         +----+
+		// |         | 22 |
+		// +----+----+----+
+		// | 30 | 31 | 32 |
+		// +----+----+----+
 		testWalker( [
 			[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 			[ '12' ],
 			[ '22' ],
 			[ '30', '31', '32' ]
 		], [
-			{ row: 0, column: 0, index: 0, data: '00' },
-			{ row: 0, column: 2, index: 1, data: '02' },
-			{ row: 1, column: 2, index: 0, data: '12' },
-			{ row: 2, column: 2, index: 0, data: '22' },
-			{ row: 3, column: 0, index: 0, data: '30' },
-			{ row: 3, column: 1, index: 1, data: '31' },
-			{ row: 3, column: 2, index: 2, data: '32' }
+			{ row: 0, column: 0, index: 0, data: '00', isAnchor: true, width: 2, height: 3 },
+			{ row: 0, column: 2, index: 1, data: '02', isAnchor: true },
+			{ row: 1, column: 2, index: 0, data: '12', isAnchor: true },
+			{ row: 2, column: 2, index: 0, data: '22', isAnchor: true },
+			{ row: 3, column: 0, index: 0, data: '30', isAnchor: true },
+			{ row: 3, column: 1, index: 1, data: '31', isAnchor: true },
+			{ row: 3, column: 2, index: 2, data: '32', isAnchor: true }
 		] );
 	} );
 
-	it( 'should properly output column indexes of a table that has multiple rowspans', () => {
+	it( 'should properly output column indexes of a table that has multiple row-spans', () => {
+		// +----+----+----+
+		// | 11 | 12 | 13 |
+		// +    +----+----+
+		// |    | 22 | 23 |
+		// +    +    +----+
+		// |    |    | 33 |
+		// +----+----+----+
+		// | 41 | 42 | 43 |
+		// +----+----+----+
 		testWalker( [
 			[ { rowspan: 3, contents: '11' }, '12', '13' ],
 			[ { rowspan: 2, contents: '22' }, '23' ],
 			[ '33' ],
 			[ '41', '42', '43' ]
 		], [
-			{ row: 0, column: 0, index: 0, data: '11' },
-			{ row: 0, column: 1, index: 1, data: '12' },
-			{ row: 0, column: 2, index: 2, data: '13' },
-			{ row: 1, column: 1, index: 0, data: '22' },
-			{ row: 1, column: 2, index: 1, data: '23' },
-			{ row: 2, column: 2, index: 0, data: '33' },
-			{ row: 3, column: 0, index: 0, data: '41' },
-			{ row: 3, column: 1, index: 1, data: '42' },
-			{ row: 3, column: 2, index: 2, data: '43' }
+			{ row: 0, column: 0, index: 0, data: '11', isAnchor: true, height: 3 },
+			{ row: 0, column: 1, index: 1, data: '12', isAnchor: true },
+			{ row: 0, column: 2, index: 2, data: '13', isAnchor: true },
+			{ row: 1, column: 1, index: 0, data: '22', isAnchor: true, height: 2 },
+			{ row: 1, column: 2, index: 1, data: '23', isAnchor: true },
+			{ row: 2, column: 2, index: 0, data: '33', isAnchor: true },
+			{ row: 3, column: 0, index: 0, data: '41', isAnchor: true },
+			{ row: 3, column: 1, index: 1, data: '42', isAnchor: true },
+			{ row: 3, column: 2, index: 2, data: '43', isAnchor: true }
 		] );
 	} );
 
 	describe( 'option.startRow', () => {
 		it( 'should start iterating from given row but with cell spans properly calculated', () => {
+			// +----+----+----+
+			// | 11      | 13 |
+			// +         +----+
+			// |         | 23 |
+			// +         +----+
+			// |         | 33 |
+			// +----+----+----+
+			// | 41 | 42 | 43 |
+			// +----+----+----+
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
 				[ '23' ],
 				[ '33' ],
 				[ '41', '42', '43' ]
 			], [
-				{ row: 2, column: 2, index: 0, data: '33' },
-				{ row: 3, column: 0, index: 0, data: '41' },
-				{ row: 3, column: 1, index: 1, data: '42' },
-				{ row: 3, column: 2, index: 2, data: '43' }
+				{ row: 2, column: 2, index: 0, data: '33', isAnchor: true },
+				{ row: 3, column: 0, index: 0, data: '41', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '42', isAnchor: true },
+				{ row: 3, column: 2, index: 2, data: '43', isAnchor: true }
 			], { startRow: 2 } );
 		} );
+
+		it( 'should start iterating from given row, includeAllSlots = true', () => {
+			// +----+----+----+
+			// | 11      | 13 |
+			// +         +----+
+			// |         | 23 |
+			// +         +----+
+			// |         | 33 |
+			// +----+----+----+
+			// | 41 | 42 | 43 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
+				[ '23' ],
+				[ '33' ],
+				[ '41', '42', '43' ]
+			], [
+				{ row: 2, column: 0, index: 0, data: '11', width: 2, height: 3, anchorRow: 0 },
+				{ row: 2, column: 1, index: 0, data: '11', width: 2, height: 3, anchorRow: 0, anchorColumn: 0 },
+				{ row: 2, column: 2, index: 0, data: '33', isAnchor: true },
+				{ row: 3, column: 0, index: 0, data: '41', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '42', isAnchor: true },
+				{ row: 3, column: 2, index: 2, data: '43', isAnchor: true }
+			], { startRow: 2, includeAllSlots: true } );
+		} );
 	} );
 
 	describe( 'option.endRow', () => {
-		it( 'should stopp iterating after given row but with cell spans properly calculated', () => {
+		it( 'should stop iterating after given row but with cell spans properly calculated', () => {
+			// +----+----+----+
+			// | 11      | 13 |
+			// +         +----+
+			// |         | 23 |
+			// +         +----+
+			// |         | 33 |
+			// +----+----+----+
+			// | 41 | 42 | 43 |
+			// +----+----+----+
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
 				[ '23' ],
 				[ '33' ],
 				[ '41', '42', '43' ]
 			], [
-				{ row: 0, column: 0, index: 0, data: '11' },
-				{ row: 0, column: 2, index: 1, data: '13' },
-				{ row: 1, column: 2, index: 0, data: '23' },
-				{ row: 2, column: 2, index: 0, data: '33' }
+				{ row: 0, column: 0, index: 0, data: '11', isAnchor: true, width: 2, height: 3 },
+				{ row: 0, column: 2, index: 1, data: '13', isAnchor: true },
+				{ row: 1, column: 2, index: 0, data: '23', isAnchor: true },
+				{ row: 2, column: 2, index: 0, data: '33', isAnchor: true }
 			], { endRow: 2 } );
 		} );
 
 		it( 'should iterate over given row only', () => {
+			// +----+----+----+
+			// | 11      | 13 |
+			// +         +----+
+			// |         | 23 |
+			// +         +----+
+			// |         | 33 |
+			// +----+----+----+
+			// | 41 | 42 | 43 |
+			// +----+----+----+
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
 				[ '23' ],
 				[ '33' ],
 				[ '41', '42', '43' ]
 			], [
-				{ row: 0, column: 0, index: 0, data: '11' },
-				{ row: 0, column: 2, index: 1, data: '13' }
+				{ row: 0, column: 0, index: 0, data: '11', isAnchor: true, width: 2, height: 3 },
+				{ row: 0, column: 2, index: 1, data: '13', isAnchor: true }
 			], { endRow: 0 } );
 		} );
+
+		it( 'should stop iterating after given row, includeAllSlots = true', () => {
+			// +----+----+----+
+			// | 11      | 13 |
+			// +         +----+
+			// |         | 23 |
+			// +         +----+
+			// |         | 33 |
+			// +----+----+----+
+			// | 41 | 42 | 43 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
+				[ '23' ],
+				[ '33' ],
+				[ '41', '42', '43' ]
+			], [
+				{ row: 0, column: 0, index: 0, data: '11', width: 2, height: 3, isAnchor: true },
+				{ row: 0, column: 1, index: 0, data: '11', width: 2, height: 3, anchorColumn: 0 },
+				{ row: 0, column: 2, index: 1, data: '13', isAnchor: true },
+				{ row: 1, column: 0, index: 0, data: '11', width: 2, height: 3, anchorRow: 0 },
+				{ row: 1, column: 1, index: 0, data: '11', width: 2, height: 3, anchorRow: 0, anchorColumn: 0 },
+				{ row: 1, column: 2, index: 0, data: '23', isAnchor: true }
+			], { endRow: 1, includeAllSlots: true } );
+		} );
+	} );
+
+	describe( 'options.row', () => {
+		it( 'should iterate given row', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 1, column: 2, index: 0, data: '12', isAnchor: true }
+			], { row: 1 } );
+		} );
+
+		it( 'should iterate given row, includeAllSlots = true', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 1, column: 0, index: 0, data: '00', width: 2, height: 3, anchorRow: 0 },
+				{ row: 1, column: 1, index: 0, data: '00', width: 2, height: 3, anchorRow: 0, anchorColumn: 0 },
+				{ row: 1, column: 2, index: 0, data: '12', isAnchor: true }
+			], { row: 1, includeAllSlots: true } );
+		} );
+	} );
+
+	describe( 'options.startColumn', () => {
+		it( 'should not return the slots before startColumn', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 0, column: 2, index: 1, data: '02', isAnchor: true },
+				{ row: 1, column: 2, index: 0, data: '12', isAnchor: true },
+				{ row: 2, column: 2, index: 0, data: '22', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true },
+				{ row: 3, column: 2, index: 2, data: '32', isAnchor: true }
+			], { startColumn: 1 } );
+		} );
+
+		it( 'should not return the slots before startColumn, includeAllSlots = true', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 0, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0 },
+				{ row: 0, column: 2, index: 1, data: '02', isAnchor: true },
+				{ row: 1, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0, anchorRow: 0 },
+				{ row: 1, column: 2, index: 0, data: '12', isAnchor: true },
+				{ row: 2, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0, anchorRow: 0 },
+				{ row: 2, column: 2, index: 0, data: '22', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true },
+				{ row: 3, column: 2, index: 2, data: '32', isAnchor: true }
+			], { startColumn: 1, includeAllSlots: true } );
+		} );
 	} );
 
-	describe( 'option.includeSpanned', () => {
+	describe( 'options.endColumn', () => {
+		it( 'should not return the slots after endColumn', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 0, column: 0, index: 0, data: '00', isAnchor: true, width: 2, height: 3 },
+				{ row: 3, column: 0, index: 0, data: '30', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true }
+			], { endColumn: 1 } );
+		} );
+
+		it( 'should not return the slots after endColumn, includeAllSlots = true', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 0, column: 0, index: 0, data: '00', width: 2, height: 3, isAnchor: true },
+				{ row: 0, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0 },
+				{ row: 1, column: 0, index: 0, data: '00', width: 2, height: 3, anchorRow: 0 },
+				{ row: 1, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0, anchorRow: 0 },
+				{ row: 2, column: 0, index: 0, data: '00', width: 2, height: 3, anchorRow: 0 },
+				{ row: 2, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0, anchorRow: 0 },
+				{ row: 3, column: 0, index: 0, data: '30', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true }
+			], { endColumn: 1, includeAllSlots: true } );
+		} );
+	} );
+
+	describe( 'options.column', () => {
+		it( 'should return the slots from given column', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true }
+			], { column: 1 } );
+		} );
+
+		it( 'should return the slots from given column, includeAllSlots = true', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
+			testWalker( [
+				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
+				[ '12' ],
+				[ '22' ],
+				[ '30', '31', '32' ]
+			], [
+				{ row: 0, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0 },
+				{ row: 1, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0, anchorRow: 0 },
+				{ row: 2, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0, anchorRow: 0 },
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true }
+			], { column: 1, includeAllSlots: true } );
+		} );
+	} );
+
+	describe( 'option.includeAllSlots', () => {
 		it( 'should output spanned cells at the end of a table', () => {
+			// +----+----+
+			// | 00 | 01 |
+			// +----+    +
+			// | 10 |    |
+			// +----+----+
 			testWalker( [
 				[ '00', { rowspan: 2, contents: '01' } ],
 				[ '10' ]
 			], [
-				{ row: 0, column: 0, index: 0, data: '00' },
-				{ row: 0, column: 1, index: 1, data: '01' },
-				{ row: 1, column: 0, index: 0, data: '10' },
-				{ row: 1, column: 1, index: 1, data: '01', isSpanned: true }
-			], { includeSpanned: true } );
+				{ row: 0, column: 0, index: 0, data: '00', isAnchor: true },
+				{ row: 0, column: 1, index: 1, data: '01', isAnchor: true, height: 2 },
+				{ row: 1, column: 0, index: 0, data: '10', isAnchor: true },
+				{ row: 1, column: 1, index: 1, data: '01', anchorRow: 0, height: 2 }
+			], { includeAllSlots: true } );
 		} );
 
 		it( 'should output spanned cells', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31      |
+			// +----+----+----+
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 				[ '12' ],
 				[ '22' ],
 				[ '30', { colspan: 2, contents: '31' } ]
 			], [
-				{ row: 0, column: 0, index: 0, data: '00' },
-				{ row: 0, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 0, column: 2, index: 1, data: '02' },
-				{ row: 1, column: 0, index: 0, data: '00', isSpanned: true },
-				{ row: 1, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 1, column: 2, index: 0, data: '12' },
-				{ row: 2, column: 0, index: 0, data: '00', isSpanned: true },
-				{ row: 2, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 2, column: 2, index: 0, data: '22' },
-				{ row: 3, column: 0, index: 0, data: '30' },
-				{ row: 3, column: 1, index: 1, data: '31' },
-				{ row: 3, column: 2, index: 1, data: '31', isSpanned: true }
-			], { includeSpanned: true } );
+				{ row: 0, column: 0, index: 0, data: '00', width: 2, height: 3, isAnchor: true },
+				{ row: 0, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0 },
+				{ row: 0, column: 2, index: 1, data: '02', isAnchor: true },
+				{ row: 1, column: 0, index: 0, data: '00', width: 2, height: 3, anchorRow: 0 },
+				{ row: 1, column: 1, index: 0, data: '00', width: 2, height: 3, anchorRow: 0, anchorColumn: 0 },
+				{ row: 1, column: 2, index: 0, data: '12', isAnchor: true },
+				{ row: 2, column: 0, index: 0, data: '00', width: 2, height: 3, anchorRow: 0 },
+				{ row: 2, column: 1, index: 0, data: '00', width: 2, height: 3, anchorRow: 0, anchorColumn: 0 },
+				{ row: 2, column: 2, index: 0, data: '22', isAnchor: true },
+				{ row: 3, column: 0, index: 0, data: '30', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '31', width: 2, isAnchor: true },
+				{ row: 3, column: 2, index: 1, data: '31', width: 2, anchorColumn: 1 }
+			], { includeAllSlots: true } );
 		} );
 
 		it( 'should output rowspanned cells at the end of a table row', () => {
+			// +----+----+
+			// | 00 | 01 |
+			// +----+    +
+			// | 10 |    |
+			// +----+----+
 			testWalker( [
 				[ '00', { rowspan: 2, contents: '01' } ],
 				[ '10' ]
 			], [
-				{ row: 0, column: 0, index: 0, data: '00' },
-				{ row: 0, column: 1, index: 1, data: '01' },
-				{ row: 1, column: 0, index: 0, data: '10' },
-				{ row: 1, column: 1, index: 1, data: '01', isSpanned: true }
-			], { includeSpanned: true } );
+				{ row: 0, column: 0, index: 0, data: '00', isAnchor: true },
+				{ row: 0, column: 1, index: 1, data: '01', isAnchor: true, height: 2 },
+				{ row: 1, column: 0, index: 0, data: '10', isAnchor: true },
+				{ row: 1, column: 1, index: 1, data: '01', anchorRow: 0, height: 2 }
+			], { includeAllSlots: true } );
 		} );
 
 		it( 'should work with startRow & endRow options', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 				[ '12' ],
 				[ '22' ],
 				[ '30', '31', '32' ]
 			], [
-				{ row: 1, column: 0, index: 0, data: '00', isSpanned: true },
-				{ row: 1, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 1, column: 2, index: 0, data: '12' },
-				{ row: 2, column: 0, index: 0, data: '00', isSpanned: true },
-				{ row: 2, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 2, column: 2, index: 0, data: '22' }
-			], { includeSpanned: true, startRow: 1, endRow: 2 } );
+				{ row: 1, column: 0, index: 0, data: '00', anchorRow: 0, width: 2, height: 3 },
+				{ row: 1, column: 1, index: 0, data: '00', anchorRow: 0, width: 2, height: 3, anchorColumn: 0 },
+				{ row: 1, column: 2, index: 0, data: '12', isAnchor: true },
+				{ row: 2, column: 0, index: 0, data: '00', anchorRow: 0, width: 2, height: 3 },
+				{ row: 2, column: 1, index: 0, data: '00', anchorRow: 0, width: 2, height: 3, anchorColumn: 0 },
+				{ row: 2, column: 2, index: 0, data: '22', isAnchor: true }
+			], { includeAllSlots: true, startRow: 1, endRow: 2 } );
 		} );
 
-		it( 'should output rowspanned cells at the end of a table row with startRow & endRow options', () => {
+		it( 'should output row-spanned cells at the end of a table row with startRow & endRow options', () => {
+			// +----+----+
+			// | 00 | 01 |
+			// +----+    +
+			// | 10 |    |
+			// +----+----+
+			// | 20 | 21 |
+			// +----+----+
 			testWalker( [
 				[ '00', { rowspan: 2, contents: '01' } ],
 				[ '10' ],
 				[ '20', '21' ]
 			], [
-				{ row: 0, column: 0, index: 0, data: '00' },
-				{ row: 0, column: 1, index: 1, data: '01' },
-				{ row: 1, column: 0, index: 0, data: '10' },
-				{ row: 1, column: 1, index: 1, data: '01', isSpanned: true }
-			], { startRow: 0, endRow: 1, includeSpanned: true } );
+				{ row: 0, column: 0, index: 0, data: '00', isAnchor: true },
+				{ row: 0, column: 1, index: 1, data: '01', isAnchor: true, height: 2 },
+				{ row: 1, column: 0, index: 0, data: '10', isAnchor: true },
+				{ row: 1, column: 1, index: 1, data: '01', anchorRow: 0, height: 2 }
+			], { startRow: 0, endRow: 1, includeAllSlots: true } );
 		} );
 	} );
 
-	describe( 'options.startColumn', () => {
-		it( 'should output only cells on given column', () => {
+	describe( '#skipRow()', () => {
+		it( 'should skip row', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 				[ '12' ],
 				[ '22' ],
 				[ '30', '31', '32' ]
 			], [
-				{ row: 3, column: 1, index: 1, data: '31' }
-			], { column: 1 } );
+				{ row: 0, column: 0, index: 0, data: '00', isAnchor: true, width: 2, height: 3 },
+				{ row: 0, column: 2, index: 1, data: '02', isAnchor: true },
+				{ row: 2, column: 2, index: 0, data: '22', isAnchor: true },
+				{ row: 3, column: 0, index: 0, data: '30', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true },
+				{ row: 3, column: 2, index: 2, data: '32', isAnchor: true }
+			], {}, 1 );
 		} );
 
-		it( 'should output only cells on given column, includeSpanned = true', () => {
+		it( 'should skip row, includeAllSlots = true', () => {
+			// +----+----+----+
+			// | 00      | 02 |
+			// +         +----+
+			// |         | 12 |
+			// +         +----+
+			// |         | 22 |
+			// +----+----+----+
+			// | 30 | 31 | 32 |
+			// +----+----+----+
 			testWalker( [
 				[ { colspan: 2, rowspan: 3, contents: '00' }, '02' ],
 				[ '12' ],
 				[ '22' ],
 				[ '30', '31', '32' ]
 			], [
-				{ row: 0, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 1, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 2, column: 1, index: 0, data: '00', isSpanned: true },
-				{ row: 3, column: 1, index: 1, data: '31' }
-			], { column: 1, includeSpanned: true } );
+				{ row: 0, column: 0, index: 0, data: '00', width: 2, height: 3, isAnchor: true },
+				{ row: 0, column: 1, index: 0, data: '00', width: 2, height: 3, anchorColumn: 0 },
+				{ row: 0, column: 2, index: 1, data: '02', isAnchor: true },
+				{ row: 2, column: 0, index: 0, data: '00', width: 2, height: 3, anchorRow: 0 },
+				{ row: 2, column: 1, index: 0, data: '00', width: 2, height: 3, anchorRow: 0, anchorColumn: 0 },
+				{ row: 2, column: 2, index: 0, data: '22', isAnchor: true },
+				{ row: 3, column: 0, index: 0, data: '30', isAnchor: true },
+				{ row: 3, column: 1, index: 1, data: '31', isAnchor: true },
+				{ row: 3, column: 2, index: 2, data: '32', isAnchor: true }
+			], { includeAllSlots: true }, 1 );
 		} );
 	} );
+
+	it( 'should throw error if walker value old api used', () => {
+		setData( model, modelTable( [
+			[ 'a' ]
+		] ) );
+
+		const walker = new TableWalker( root.getChild( 0 ) );
+
+		const { value } = walker.next();
+
+		expect( () => value.isSpanned ).to.throw( CKEditorError, 'tablewalker-improper-api-usage' );
+		expect( () => value.colspan ).to.throw( CKEditorError, 'tablewalker-improper-api-usage' );
+		expect( () => value.rowspan ).to.throw( CKEditorError, 'tablewalker-improper-api-usage' );
+		expect( () => value.cellIndex ).to.throw( CKEditorError, 'tablewalker-improper-api-usage' );
+	} );
 } );