Przeglądaj źródła

TableWalker refactored, API changes.

Kuba Niegowski 5 lat temu
rodzic
commit
e5ed0836a2

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

+ 2 - 2
packages/ckeditor5-table/src/converters/downcast.js

@@ -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,
@@ -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,

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

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

@@ -250,7 +250,7 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
 	const selectedTableMap = [ ...new TableWalker( selectedTable, {
 		startRow: firstRowOfSelection,
 		endRow: lastRowOfSelection,
-		includeSpanned: true
+		includeAllSlots: true
 	} ) ];
 
 	// Selection must be set to pasted cells (some might be removed or new created).
@@ -265,7 +265,7 @@ 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 ) {
+	for ( const { row, column, cell, isAnchor } of selectedTableMap ) {
 		if ( column === 0 ) {
 			previousCellInRow = null;
 		}
@@ -273,7 +273,7 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
 		// 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 ) {
+			if ( isAnchor ) {
 				previousCellInRow = cell;
 			}
 
@@ -284,7 +284,7 @@ function replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selected
 		// 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 );
 		}
 
@@ -460,7 +460,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 +476,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 );
@@ -512,8 +512,7 @@ function isAffectedBySelection( index, span, limit ) {
 //    +---+---+---+---+
 function adjustLastRowIndex( table, rowIndexes, columnIndexes ) {
 	const tableIterator = new TableWalker( table, {
-		startRow: rowIndexes.last,
-		endRow: rowIndexes.last
+		row: rowIndexes.last
 	} );
 
 	const lastRowMap = Array.from( tableIterator ).filter( ( { column } ) => {
@@ -521,7 +520,7 @@ function adjustLastRowIndex( table, rowIndexes, columnIndexes ) {
 		return columnIndexes.first <= column && column <= columnIndexes.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 +528,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 +556,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 +564,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;
 		}
 

+ 3 - 3
packages/ckeditor5-table/src/tableselection/croptable.js

@@ -58,16 +58,16 @@ export function cropTableToDimensions( sourceTable, cropDimensions, writer, tabl
 		writer.insertElement( 'tableRow', croppedTable, 'end' );
 	}
 
-	const tableMap = [ ...new TableWalker( sourceTable, { startRow, endRow, startColumn, endColumn, 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 ) {
+	for ( const { row: sourceRow, column: sourceColumn, cell: tableCell, isAnchor } 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 ) {
+		if ( !isAnchor ) {
 			// TODO: Remove table utils usage. See: https://github.com/ckeditor/ckeditor5/issues/6785.
 			const { row: anchorRow, column: anchorColumn } = tableUtils.getCellLocation( tableCell );
 

+ 29 - 30
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,12 +244,12 @@ 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 { 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 );
@@ -260,10 +260,10 @@ export default class TableUtils extends Plugin {
 					// For such cells expand them by a number of columns inserted.
 					writer.setAttribute( 'colspan', colspan + columnsToInsert, cell );
 
-					// The `includeSpanned` option will output the "empty"/spanned column so skip this row already.
+					// The `includeAllSlots` 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 also (because of `includeSpanned` option) - (cell "a")
+					// This cell will overlap cells in rows below so skip them also (because of `includeAllSlots` option) - (cell "a")
 					if ( rowspan > 1 ) {
 						for ( let i = row + 1; i < row + rowspan; i++ ) {
 							tableWalker.skipRow( i );
@@ -388,10 +388,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 +499,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 +608,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.
@@ -659,12 +659,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 +829,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 +860,7 @@ function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
 
 			cellsToTrim.push( {
 				cell,
-				rowspan: rowspan - rowspanAdjustment
+				rowspan: cellHeight - rowspanAdjustment
 			} );
 		}
 	}
@@ -869,9 +869,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 +878,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 +890,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;
 		}

+ 153 - 66
packages/ckeditor5-table/src/tablewalker.js

@@ -20,7 +20,7 @@ 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.
 	 *
@@ -55,10 +55,10 @@ 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' ) );
+	 *			console.log( 'Cell at ' + value.row + ' x ' + value.column + ' : ' + ( !value.isAnchor ? 'is spanned' : 'has data' ) );
 	 *		}
 	 *
 	 * will log in the console for the table from the previous example:
@@ -76,7 +76,7 @@ export default class TableWalker {
 	 * @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.
+	 * @param {Boolean} [options.includeAllSlots=false] Also return values for spanned cells.
 	 */
 	constructor( table, options = {} ) {
 		/**
@@ -103,13 +103,21 @@ export default class TableWalker {
 		 */
 		this.endRow = typeof options.endRow == 'number' ? options.endRow : undefined;
 
+		/**
+		 * If set, the table walker will only output cells of a given row or cells that overlap it.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.row = typeof options.row == 'number' ? options.row : undefined;
+
 		/**
 		 * Enables output of spanned cells that are normally not yielded.
 		 *
 		 * @readonly
 		 * @member {Boolean}
 		 */
-		this.includeSpanned = !!options.includeSpanned;
+		this.includeAllSlots = !!options.includeAllSlots;
 
 		/**
 		 * If set, the table walker will only output cells from a given column and following ones or cells that overlap them.
@@ -131,7 +139,6 @@ export default class TableWalker {
 		 * If set, the table walker will only output cells of a given column or cells that overlap it.
 		 *
 		 * @readonly
-		 * @deprecated
 		 * @member {Number}
 		 */
 		this.column = typeof options.column == 'number' ? options.column : undefined;
@@ -164,7 +171,7 @@ export default class TableWalker {
 		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
@@ -173,14 +180,6 @@ export default class TableWalker {
 		 */
 		this._cellIndex = 0;
 
-		/**
-		 * The previous cell in the current row.
-		 *
-		 * @member {module:engine/model/element~Element}
-		 * @private
-		 */
-		this._previousCellInRow = null;
-
 		/**
 		 * Holds a map of spanned cells in a table.
 		 *
@@ -204,6 +203,20 @@ export default class TableWalker {
 		return this;
 	}
 
+	set row( value ) {
+		if ( typeof value == 'number' ) {
+			this.startRow = this.endRow = value;
+		}
+	}
+
+	get row() {
+		if ( this.startRow === this.endRow ) {
+			return this.startRow;
+		}
+
+		throw new CKEditorError( 'improper-api-usage', this );
+	}
+
 	set column( value ) {
 		if ( typeof value == 'number' ) {
 			this.startColumn = this.endColumn = value;
@@ -215,7 +228,7 @@ export default class TableWalker {
 			return this.startColumn;
 		}
 
-		throw new CKEditorError( 'improper-use-of-deprecated-api', this );
+		throw new CKEditorError( 'improper-api-usage', this );
 	}
 
 	/**
@@ -235,15 +248,16 @@ export default class TableWalker {
 			return this._advanceToNextRow();
 		}
 
-		let cell, skipCurrentValue, outValue;
+		let outValue = null;
 
 		const spanData = this._getSpanned();
 
 		if ( spanData ) {
-			skipCurrentValue = !this.includeSpanned || this._shouldSkipRow() || this._shouldSkipColumn();
-			outValue = this._formatOutValue( spanData.cell, 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.
@@ -255,14 +269,14 @@ export default class TableWalker {
 
 			// Record this cell spans if it's not 1x1 cell.
 			if ( colspan > 1 || rowspan > 1 ) {
-				this._recordSpans( rowspan, colspan, cell );
+				this._recordSpans( cell, rowspan, colspan );
 			}
 
-			skipCurrentValue = this._shouldSkipRow() || this._shouldSkipColumn();
-			outValue = this._formatOutValue( cell, null, rowspan, colspan );
+			if ( !this._shouldSkipSlot() ) {
+				outValue = this._formatOutValue( cell );
+			}
 
 			this._nextCellAtColumn = this._column + colspan;
-			this._previousCellInRow = cell;
 		}
 
 		// Advance to the next column before returning value.
@@ -273,7 +287,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();
 	}
 
 	/**
@@ -314,7 +328,6 @@ export default class TableWalker {
 		this._column = 0;
 		this._cellIndex = 0;
 		this._nextCellAtColumn = -1;
-		this._previousCellInRow = null;
 
 		return this.next();
 	}
@@ -324,53 +337,37 @@ export default class TableWalker {
 	 *
 	 * @private
 	 * @param {module:engine/model/element~Element} cell The table cell to output.
-	 * @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.
+	 * @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}}}
 	 */
-	_formatOutValue( cell, spanData, rowspan = 1, colspan = 1 ) {
+	_formatOutValue( cell, anchorRow = this._row, anchorColumn = this._column ) {
 		return {
 			done: false,
-			value: {
-				cell,
+			value: new TableWalkerValue( cell, {
 				row: this._row,
 				column: this._column,
-				anchorRow: spanData ? spanData.row : this._row,
-				anchorColumn: spanData ? spanData.column : this._column,
-				isSpanned: Boolean( spanData ),
-				rowspan,
-				colspan,
-				cellIndex: this._cellIndex,
-				previousCellInRow: this._previousCellInRow
-			}
+				anchorRow,
+				anchorColumn,
+				cellIndex: this._cellIndex
+			} )
 		};
 	}
 
 	/**
-	 * Checks if the current row should be skipped.
+	 * Checks if the current slot should be skipped.
 	 *
 	 * @private
 	 * @returns {Boolean}
 	 */
-	_shouldSkipRow() {
+	_shouldSkipSlot() {
 		const rowIsBelowStartRow = this._row < this.startRow;
 		const rowIsMarkedAsSkipped = this._skipRows.has( this._row );
 
-		return rowIsBelowStartRow || rowIsMarkedAsSkipped;
-	}
-
-	/**
-	 * Checks if the current column should be skipped.
-	 *
-	 * @private
-	 * @returns {Boolean}
-	 */
-	_shouldSkipColumn() {
 		const columnIsBeforeStartColumn = this.startColumn !== undefined && this._column < this.startColumn;
 		const columnIsAfterEndColumn = this.endColumn !== undefined && this._column > this.endColumn;
 
-		return columnIsBeforeStartColumn || columnIsAfterEndColumn;
+		return rowIsBelowStartRow || rowIsMarkedAsSkipped || columnIsBeforeStartColumn || columnIsAfterEndColumn;
 	}
 
 	/**
@@ -395,11 +392,11 @@ export default class TableWalker {
 	 * Updates spanned cells map relative to the current cell location and its span dimensions.
 	 *
 	 * @private
+	 * @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( rowspan, colspan, cell ) {
+	_recordSpans( cell, rowspan, colspan ) {
 		const data = {
 			cell,
 			row: this._row,
@@ -436,15 +433,105 @@ export default class TableWalker {
 
 /**
  * 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 TableWalkerValue {
+	/**
+	 * Creates an instance of the table walker value.
+	 *
+	 * @param {module:engine/model/element~Element} cell The current table cell.
+	 * @param {Object} data
+	 * @param {Number} data.row The row index of a table slot.
+	 * @param {Number} data.column The column index of a table slot.
+	 * @param {Number} data.anchorRow The row index of a cell anchor slot.
+	 * @param {Number} data.anchorColumn The column index of a cell anchor slot.
+	 * @param {Number} data.cellIndex The index of the current cell in the parent row.
+	 */
+	constructor( cell, data ) {
+		/**
+		 * 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 = data.row;
+
+		/**
+		 * The column index of a table slot.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.column = data.column;
+
+		/**
+		 * The row index of a cell anchor slot.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.cellAnchorRow = data.anchorRow;
+
+		/**
+		 * The column index of a cell anchor slot.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.cellAnchorColumn = data.anchorColumn;
+
+		/**
+		 * The index of the current cell in the parent row.
+		 *
+		 * @readonly
+		 * @member {Number}
+		 */
+		this.cellIndex = data.cellIndex;
+	}
+
+	/**
+	 * Whether the cell is anchored in the current slot.
+	 *
+	 * @returns {Boolean}
+	 */
+	get isAnchor() {
+		return this.row === this.cellAnchorRow && this.column === this.cellAnchorColumn;
+	}
+
+	/**
+	 * The `colspan` attribute of a cell. If the model attribute is not present, it is set to `1`.
+	 *
+	 * @returns {Number}
+	 */
+	get cellWidth() {
+		return parseInt( this.cell.getAttribute( 'colspan' ) || 1 );
+	}
+
+	/**
+	 * The `rowspan` attribute of a cell. If the model attribute is not present, it is set to `1`.
+	 *
+	 * @returns {Number}
+	 */
+	get cellHeight() {
+		return parseInt( this.cell.getAttribute( 'rowspan' ) || 1 );
+	}
+
+	get isSpanned() {
+		throw new CKEditorError( 'improper-api-usage', this );
+	}
+
+	get colspan() {
+		throw new CKEditorError( 'improper-api-usage', this );
+	}
+
+	get rowspan() {
+		throw new CKEditorError( 'improper-api-usage', this );
+	}
+}

+ 5 - 5
packages/ckeditor5-table/src/utils.js

@@ -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,7 +318,7 @@ 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;
 
@@ -371,8 +371,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 );

+ 8 - 8
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,8 +519,8 @@ export function createTableAsciiArt( model, table ) {
 		for ( let column = 0; column <= lastColumn; column++ ) {
 			const cellInfo = tableMap[ row * columns + column ];
 
-			const isColSpan = cellInfo.anchorColumn != cellInfo.column;
-			const isRowSpan = cellInfo.anchorRow != cellInfo.row;
+			const isColSpan = cellInfo.cellAnchorColumn != cellInfo.column;
+			const isRowSpan = cellInfo.cellAnchorRow != cellInfo.row;
 
 			gridLine += !isColSpan || !isRowSpan ? '+' : ' ';
 			gridLine += !isRowSpan ? '----' : '    ';
@@ -570,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 );

+ 357 - 104
packages/ckeditor5-table/tests/tablewalker.js

@@ -27,241 +27,456 @@ 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 result = [];
-
-		for ( const tableInfo of iterator ) {
-			result.push( tableInfo );
+		const walker = new TableWalker( root.getChild( 0 ), options );
+		if ( skip !== undefined ) {
+			walker.skipRow( skip );
 		}
-
-		const formattedResult = result.map( ( { row, column, isSpanned, cell, cellIndex } ) => {
-			const result = {
-				row,
-				column,
-				data: cell && cell.getChild( 0 ).getChild( 0 ).data,
-				index: cellIndex
-			};
-
-			if ( isSpanned ) {
-				result.isSpanned = true;
-			}
-
-			return result;
-		} );
+		const result = [ ...walker ];
+
+		const formattedResult = result.map( ( { cell, row, column, isAnchor, cellWidth, cellHeight, cellIndex, ...anchor } ) => ( {
+			row,
+			column,
+			data: cell && cell.getChild( 0 ).getChild( 0 ).data,
+			index: cellIndex,
+			...( anchor.cellAnchorRow != row ? { anchorRow: anchor.cellAnchorRow } : null ),
+			...( anchor.cellAnchorColumn != column ? { anchorColumn: anchor.cellAnchorColumn } : null ),
+			...( isAnchor ? { isAnchor } : null ),
+			...( cellWidth > 1 ? { width: cellWidth } : null ),
+			...( cellHeight > 1 ? { height: cellHeight } : null )
+		} ) );
 
 		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', () => {
+		// +----+----+----+
+		// | 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', () => {
+		// +----+----+----+
+		// | 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', () => {
+		// +----+----+----+
+		// | 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 } );
 		} );
 	} );
 
 	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 } );
 		} );
 	} );
 
-	describe( 'option.includeSpanned', () => {
+	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( '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( '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', () => {
+			// +----+----+
+			// | 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 return deprecated column option value', () => {
+	it( 'should return column option value', () => {
 		setData( model, modelTable( [
 			[ 'a' ]
 		] ) );
@@ -269,15 +484,53 @@ describe( 'TableWalker', () => {
 		const walker = new TableWalker( root.getChild( 0 ), { column: 7 } );
 
 		expect( walker.column ).to.equal( 7 );
+		expect( walker.startColumn ).to.equal( 7 );
+		expect( walker.endColumn ).to.equal( 7 );
 	} );
 
-	it( 'should throw error if deprecated api used improperly', () => {
+	it( 'should throw error if column api used improperly', () => {
 		setData( model, modelTable( [
 			[ 'a' ]
 		] ) );
 
 		const walker = new TableWalker( root.getChild( 0 ), { startColumn: 1, endColumn: 2 } );
 
-		expect( () => walker.column ).to.throw( CKEditorError, 'improper-use-of-deprecated-api' );
+		expect( () => walker.column ).to.throw( CKEditorError, 'improper-api-usage' );
+	} );
+
+	it( 'should return row option value', () => {
+		setData( model, modelTable( [
+			[ 'a' ]
+		] ) );
+
+		const walker = new TableWalker( root.getChild( 0 ), { row: 7 } );
+
+		expect( walker.row ).to.equal( 7 );
+		expect( walker.startRow ).to.equal( 7 );
+		expect( walker.endRow ).to.equal( 7 );
+	} );
+
+	it( 'should throw error if row api used improperly', () => {
+		setData( model, modelTable( [
+			[ 'a' ]
+		] ) );
+
+		const walker = new TableWalker( root.getChild( 0 ), { startRow: 1, endRow: 2 } );
+
+		expect( () => walker.row ).to.throw( CKEditorError, 'improper-api-usage' );
+	} );
+
+	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, 'improper-api-usage' );
+		expect( () => value.colspan ).to.throw( CKEditorError, 'improper-api-usage' );
+		expect( () => value.rowspan ).to.throw( CKEditorError, 'improper-api-usage' );
 	} );
 } );