瀏覽代碼

Added: Introduce TableIterator.

Maciej Gołaszewski 7 年之前
父節點
當前提交
34380426ea

+ 54 - 134
packages/ckeditor5-table/src/converters/downcasttable.js

@@ -9,7 +9,7 @@
 
 import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
-import CellSpans from '../cellspans';
+import TableIterator from './../tableiterator';
 
 /**
  * Model table element to view table element conversion helper.
@@ -35,20 +35,22 @@ export default function downcastTable() {
 
 		const tableElement = conversionApi.writer.createContainerElement( 'table' );
 		const headingRows = getNumericAttribute( table, 'headingRows', 0 );
-		const tableRows = Array.from( table.getChildren() );
+		const headingColumns = getNumericAttribute( table, 'headingColumns', 0 );
+
+		const tableIterator = new TableIterator( table );
 
-		const cellSpans = getCellSpansWithPreviousFilled( table, 0 );
+		for ( const tableCellInfo of tableIterator.iterateOver() ) {
+			const { row, column, cell: tableCell } = tableCellInfo;
 
-		for ( const tableRow of tableRows ) {
-			const rowIndex = tableRows.indexOf( tableRow );
-			const isHead = headingRows && rowIndex < headingRows;
+			const isHead = headingRows && row < headingRows;
 
 			const tableSectionElement = getTableSection( isHead ? 'thead' : 'tbody', tableElement, conversionApi, tableSections );
+			const tableRow = table.getChild( row );
 
-			downcastTableRow( tableRow, rowIndex, tableSectionElement, cellSpans, conversionApi );
+			// Check if row was converted
+			const trElement = getOrCreateTr( tableRow, row, tableSectionElement, conversionApi );
 
-			// Drop table cell spans information for downcasted row.
-			cellSpans.drop( rowIndex );
+			downcastTableCell( tableCell, getCellElementName( row, column, headingRows, headingColumns ), trElement, conversionApi );
 		}
 
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
@@ -78,6 +80,7 @@ export function downcastInsertRow() {
 		const tableElement = conversionApi.mapper.toViewElement( table );
 
 		const headingRows = getNumericAttribute( table, 'headingRows', 0 );
+		const headingColumns = getNumericAttribute( table, 'headingColumns', 0 );
 
 		const rowIndex = table.getChildIndex( tableRow );
 		const isHeadingRow = rowIndex < headingRows;
@@ -85,9 +88,16 @@ export function downcastInsertRow() {
 		const tableSection = Array.from( tableElement.getChildren() )
 			.filter( child => child.name === ( isHeadingRow ? 'thead' : 'tbody' ) )[ 0 ];
 
-		const cellSpans = getCellSpansWithPreviousFilled( table, rowIndex );
+		const tableIterator = new TableIterator( table );
 
-		downcastTableRow( tableRow, rowIndex, tableSection, cellSpans, conversionApi );
+		for ( const tableCellInfo of tableIterator.iterateOverRow( rowIndex ) ) {
+			const { cell: tableCell, column } = tableCellInfo;
+
+			const trElement = getOrCreateTr( tableRow, rowIndex, tableSection, conversionApi );
+			const cellElementName = getCellElementName( rowIndex, column, headingRows, headingColumns );
+
+			downcastTableCell( tableCell, cellElementName, trElement, conversionApi );
+		}
 	}, { priority: 'normal' } );
 }
 
@@ -116,21 +126,15 @@ export function downcastInsertCell() {
 
 		const rowIndex = table.getChildIndex( tableRow );
 
-		const cellIndex = tableRow.getChildIndex( tableCell );
+		const tableIterator = new TableIterator( table );
 
-		let columnIndex = 0;
+		for ( const { cell, column } of tableIterator.iterateOverRow( rowIndex ) ) {
+			if ( cell === tableCell ) {
+				const cellElementName = getCellElementName( rowIndex, column, headingRows, headingColumns );
 
-		const cellSpans = getCellSpansWithPreviousFilled( table, rowIndex, columnIndex );
-
-		// check last row up to
-		columnIndex = getColumnIndex( tableRow, columnIndex, cellSpans, rowIndex, tableCell );
-
-		// Check whether current columnIndex is overlapped by table cells from previous rows.
-		columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
-
-		const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
-
-		downcastTableCell( tableCell, rowIndex, columnIndex, cellSpans, cellElementName, trElement, conversionApi, cellIndex );
+				downcastTableCell( tableCell, cellElementName, trElement, conversionApi, tableRow.getChildIndex( tableCell ) );
+			}
+		}
 	}, { priority: 'normal' } );
 }
 
@@ -156,18 +160,16 @@ export function downcastAttributeChange( attribute ) {
 		const headingColumns = getNumericAttribute( table, 'headingColumns', 0 );
 		const tableElement = conversionApi.mapper.toViewElement( table );
 
-		const tableRows = Array.from( table.getChildren() );
-
-		const cellSpans = getCellSpansWithPreviousFilled( table, 0 );
-
 		const cachedTableSections = {};
 
-		for ( const tableRow of tableRows ) {
-			const rowIndex = tableRows.indexOf( tableRow );
+		const tableIterator = new TableIterator( table );
+		for ( const tableCellInfo of tableIterator.iterateOver() ) {
+			const { row, column, cell } = tableCellInfo;
+			const tableRow = table.getChild( row );
 
 			const tr = conversionApi.mapper.toViewElement( tableRow );
 
-			const desiredParentName = rowIndex < headingRows ? 'thead' : 'tbody';
+			const desiredParentName = row < headingRows ? 'thead' : 'tbody';
 
 			if ( desiredParentName !== tr.parent.name ) {
 				const tableSection = getTableSection( desiredParentName, tableElement, conversionApi, cachedTableSections );
@@ -175,12 +177,12 @@ export function downcastAttributeChange( attribute ) {
 				let targetPosition;
 
 				if ( desiredParentName === 'tbody' &&
-					rowIndex === data.attributeNewValue &&
+					row === data.attributeNewValue &&
 					data.attributeNewValue < data.attributeOldValue
 				) {
 					targetPosition = ViewPosition.createAt( tableSection, 'start' );
-				} else if ( rowIndex > 0 ) {
-					const previousTr = conversionApi.mapper.toViewElement( table.getChild( rowIndex - 1 ) );
+				} else if ( row > 0 ) {
+					const previousTr = conversionApi.mapper.toViewElement( table.getChild( row - 1 ) );
 
 					targetPosition = ViewPosition.createAfter( previousTr );
 				} else {
@@ -190,28 +192,17 @@ export function downcastAttributeChange( attribute ) {
 				conversionApi.writer.move( ViewRange.createOn( tr ), targetPosition );
 			}
 
-			// Check rows
-			let columnIndex = 0;
-
-			for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
-				// Check whether current columnIndex is overlapped by table cells from previous rows.
-				columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
+			// Check whether current columnIndex is overlapped by table cells from previous rows.
 
-				const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
+			const cellElementName = getCellElementName( row, column, headingRows, headingColumns );
 
-				const cell = conversionApi.mapper.toViewElement( tableCell );
+			const viewCell = conversionApi.mapper.toViewElement( cell );
 
-				// If in single change we're converting attribute changes and inserting cell the table cell might not be inserted into view
-				// because of child conversion is done after parent.
-				if ( cell && cell.name !== cellElementName ) {
-					conversionApi.writer.rename( cell, cellElementName );
-				}
-
-				columnIndex = columnIndex + getNumericAttribute( tableCell, 'colspan', 1 );
+			// If in single change we're converting attribute changes and inserting cell the table cell might not be inserted into view
+			// because of child conversion is done after parent.
+			if ( viewCell && viewCell.name !== cellElementName ) {
+				conversionApi.writer.rename( viewCell, cellElementName );
 			}
-
-			// Drop table cell spans information for checked rows.
-			cellSpans.drop( rowIndex );
 		}
 
 		// TODO: maybe a postfixer?
@@ -229,12 +220,7 @@ export function downcastAttributeChange( attribute ) {
 // @param {Number} rowIndex
 // @param {module:table/cellspans~CellSpans} cellSpans
 // @param {module:engine/view/containerelement~ContainerElement} tableSection
-function downcastTableCell( tableCell, rowIndex, columnIndex, cellSpans, cellElementName, trElement, conversionApi, offset = 'end' ) {
-	const colspan = getNumericAttribute( tableCell, 'colspan', 1 );
-	const rowspan = getNumericAttribute( tableCell, 'rowspan', 1 );
-
-	cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
-
+function downcastTableCell( tableCell, cellElementName, trElement, conversionApi, offset = 'end' ) {
 	// Will always consume since we're converting <tableRow> element from a parent <table>.
 	conversionApi.consumable.consume( tableCell, 'insert' );
 
@@ -242,43 +228,27 @@ function downcastTableCell( tableCell, rowIndex, columnIndex, cellSpans, cellEle
 
 	conversionApi.mapper.bindElements( tableCell, cellElement );
 	conversionApi.writer.insert( ViewPosition.createAt( trElement, offset ), cellElement );
-
-	// Skip to next "free" column index.
-	columnIndex += colspan;
-
-	return columnIndex;
 }
 
-// @param {Object} conversionApi
-function downcastTableRow( tableRow, rowIndex, tableSection, cellSpans, conversionApi ) {
+function getOrCreateTr( tableRow, rowIndex, tableSection, conversionApi ) {
 	// Will always consume since we're converting <tableRow> element from a parent <table>.
 	conversionApi.consumable.consume( tableRow, 'insert' );
 
 	const headingRows = tableRow.parent.getAttribute( 'headingRows' ) || 0;
 
-	const trElement = conversionApi.writer.createContainerElement( 'tr' );
-	conversionApi.mapper.bindElements( tableRow, trElement );
-
-	const offset = headingRows > 0 && rowIndex >= headingRows ? rowIndex - headingRows : rowIndex;
-
-	const position = ViewPosition.createAt( tableSection, offset );
-	conversionApi.writer.insert( position, trElement );
+	let trElement = conversionApi.mapper.toViewElement( tableRow );
 
-	// Defines tableCell horizontal position in table.
-	// Might be different then position of tableCell in parent tableRow
-	// as tableCells from previous rows might overlaps current row's cells.
-	let columnIndex = 0;
+	if ( !trElement ) {
+		trElement = conversionApi.writer.createContainerElement( 'tr' );
+		conversionApi.mapper.bindElements( tableRow, trElement );
 
-	const headingColumns = tableRow.parent.getAttribute( 'headingColumns' ) || 0;
+		const offset = headingRows > 0 && rowIndex >= headingRows ? rowIndex - headingRows : rowIndex;
 
-	for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
-		// Check whether current columnIndex is overlapped by table cells from previous rows.
-		columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
-
-		const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
-
-		columnIndex = downcastTableCell( tableCell, rowIndex, columnIndex, cellSpans, cellElementName, trElement, conversionApi );
+		const position = ViewPosition.createAt( tableSection, offset );
+		conversionApi.writer.insert( position, trElement );
 	}
+
+	return trElement;
 }
 
 // Returns `th` for heading cells and `td` for other cells.
@@ -373,53 +343,3 @@ function removeTableSectionIfEmpty( sectionName, tableElement, conversionApi ) {
 export function getNumericAttribute( element, attribute, defaultValue ) {
 	return element.hasAttribute( attribute ) ? parseInt( element.getAttribute( attribute ) ) : defaultValue;
 }
-
-function getColumnIndex( tableRow, columnIndex, cellSpans, rowIndex, tableCell ) {
-	for ( const tableCellA of Array.from( tableRow.getChildren() ) ) {
-		// Check whether current columnIndex is overlapped by table cells from previous rows.
-		columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
-
-		// Up to here only!
-		if ( tableCellA === tableCell ) {
-			return columnIndex;
-		}
-
-		const colspan = getNumericAttribute( tableCellA, 'colspan', 1 );
-		const rowspan = getNumericAttribute( tableCellA, 'rowspan', 1 );
-
-		cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
-
-		// Skip to next "free" column index.
-		columnIndex += colspan;
-	}
-}
-
-// Helper method for creating a pre-filled cell span instance. Useful when converting part of a table.
-//
-// @param {module:engine/model/element~Element} table Model table instance.
-// @param currentRowIndex Current row index - all table rows up to this index will be analyzed (excluding provided row index).
-// @returns {module:table/cellspans~CellSpans}
-function getCellSpansWithPreviousFilled( table, currentRowIndex ) {
-	const cellSpans = new CellSpans();
-
-	for ( let rowIndex = 0; rowIndex <= currentRowIndex; rowIndex++ ) {
-		const row = table.getChild( rowIndex );
-
-		let columnIndex = 0;
-
-		// TODO: make this an iterator?
-		for ( const tableCell of Array.from( row.getChildren() ) ) {
-			columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
-
-			const colspan = getNumericAttribute( tableCell, 'colspan', 1 );
-			const rowspan = getNumericAttribute( tableCell, 'rowspan', 1 );
-
-			cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
-
-			// Skip to next "free" column index.
-			columnIndex += colspan;
-		}
-	}
-
-	return cellSpans;
-}

+ 50 - 44
packages/ckeditor5-table/src/insertcolumncommand.js

@@ -8,9 +8,17 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import CellSpans from './cellspans';
+import TableIterator from './tableiterator';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 
+function createCells( columns, writer, insertPosition ) {
+	for ( let i = 0; i < columns; i++ ) {
+		const cell = writer.createElement( 'tableCell' );
+
+		writer.insert( cell, insertPosition );
+	}
+}
+
 /**
  * The insert column command.
  *
@@ -48,10 +56,17 @@ export default class InsertColumnCommand extends Command {
 
 		const table = getValidParent( selection.getFirstPosition() );
 
-		const cellSpans = new CellSpans();
-
 		model.change( writer => {
-			let rowIndex = 0;
+			const tableColumns = getColumns( table );
+
+			// Inserting at the end of a table
+			if ( tableColumns <= insertAt ) {
+				for ( const tableRow of table.getChildren() ) {
+					createCells( columns, writer, Position.createAt( tableRow, 'end' ) );
+				}
+
+				return;
+			}
 
 			const headingColumns = table.getAttribute( 'headingColumns' );
 
@@ -59,56 +74,36 @@ export default class InsertColumnCommand extends Command {
 				writer.setAttribute( 'headingColumns', headingColumns + columns, table );
 			}
 
-			for ( const row of table.getChildren() ) {
-				// TODO: what to do with max columns?
-
-				let columnIndex = 0;
-
-				// Cache original children.
-				const children = [ ...row.getChildren() ];
+			const tableIterator = new TableIterator( table );
 
-				for ( const tableCell of children ) {
-					let colspan = tableCell.hasAttribute( 'colspan' ) ? parseInt( tableCell.getAttribute( 'colspan' ) ) : 1;
-					const rowspan = tableCell.hasAttribute( 'rowspan' ) ? parseInt( tableCell.getAttribute( 'rowspan' ) ) : 1;
+			let currentRow = -1;
+			let currentRowInserted = false;
 
-					columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
+			for ( const tableCellInfo of tableIterator.iterateOver() ) {
+				const { row, column, cell: tableCell, colspan } = tableCellInfo;
 
-					// TODO: this is not cool:
-					const shouldExpandSpan = colspan > 1 &&
-						( columnIndex !== insertAt ) &&
-						( columnIndex <= insertAt ) &&
-						( columnIndex <= insertAt + columns ) &&
-						( columnIndex + colspan > insertAt );
-
-					if ( shouldExpandSpan ) {
-						colspan += columns;
-
-						writer.setAttribute( 'colspan', colspan, tableCell );
-					}
-
-					while ( columnIndex >= insertAt && columnIndex < insertAt + columns ) {
-						const cell = writer.createElement( 'tableCell' );
-
-						writer.insert( cell, Position.createBefore( tableCell ) );
-
-						columnIndex++;
-					}
+				if ( currentRow !== row ) {
+					currentRow = row;
+					currentRowInserted = false;
+				}
 
-					cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
+				const shouldExpandSpan = colspan > 1 &&
+					( column !== insertAt ) &&
+					( column <= insertAt ) &&
+					( column <= insertAt + columns ) &&
+					( column + colspan > insertAt );
 
-					columnIndex += colspan;
+				if ( shouldExpandSpan ) {
+					writer.setAttribute( 'colspan', colspan + columns, tableCell );
 				}
 
-				// Insert at the end of column
-				while ( columnIndex >= insertAt && columnIndex < insertAt + columns ) {
-					const cell = writer.createElement( 'tableCell' );
+				if ( column === insertAt || ( column < insertAt + columns && column > insertAt && !currentRowInserted ) ) {
+					const insertPosition = Position.createBefore( tableCell );
 
-					writer.insert( cell, row, 'end' );
+					createCells( columns, writer, insertPosition );
 
-					columnIndex++;
+					currentRowInserted = true;
 				}
-
-				rowIndex++;
 			}
 		} );
 	}
@@ -125,3 +120,14 @@ function getValidParent( firstPosition ) {
 		parent = parent.parent;
 	}
 }
+
+// TODO: dup
+function getColumns( table ) {
+	const row = table.getChild( 0 );
+
+	return [ ...row.getChildren() ].reduce( ( columns, row ) => {
+		const columnWidth = parseInt( row.getAttribute( 'colspan' ) ) || 1;
+
+		return columns + ( columnWidth );
+	}, 0 );
+}

+ 24 - 36
packages/ckeditor5-table/src/insertrowcommand.js

@@ -8,8 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getNumericAttribute } from './converters/downcasttable';
-import CellSpans from './cellspans';
+import TableIterator from './tableiterator';
 
 /**
  * The insert row command.
@@ -52,54 +51,43 @@ export default class InsertRowCommand extends Command {
 
 		const columns = getColumns( table );
 
-		const cellSpans = new CellSpans();
-
 		model.change( writer => {
 			if ( headingRows > insertAt ) {
 				writer.setAttribute( 'headingRows', headingRows + rows, table );
 			}
 
-			let tableRow;
-
-			for ( let rowIndex = 0; rowIndex < insertAt + rows; rowIndex++ ) {
-				if ( rowIndex < insertAt ) {
-					tableRow = table.getChild( rowIndex );
-
-					// Record spans, update rowspans
-					let columnIndex = 0;
+			const tableIterator = new TableIterator( table );
 
-					for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
-						columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
+			let tableCellToInsert = 0;
 
-						const colspan = getNumericAttribute( tableCell, 'colspan', 1 );
-						let rowspan = getNumericAttribute( tableCell, 'rowspan', 1 );
+			for ( const tableCellInfo of tableIterator.iterateOver() ) {
+				const { row, rowspan, colspan, cell } = tableCellInfo;
 
-						if ( rowspan > 1 ) {
-							// check whether rowspan overlaps inserts:
-							if ( rowIndex < insertAt && rowIndex + rowspan > insertAt ) {
-								rowspan = rowspan + rows;
-
-								writer.setAttribute( 'rowspan', rowspan, tableCell );
-							}
-
-							cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
+				if ( row < insertAt ) {
+					if ( rowspan > 1 ) {
+						// check whether rowspan overlaps inserts:
+						if ( row < insertAt && row + rowspan > insertAt ) {
+							writer.setAttribute( 'rowspan', rowspan + rows, cell );
 						}
-
-						columnIndex = columnIndex + colspan;
 					}
-				} else {
-					// Create new rows
-					tableRow = writer.createElement( 'tableRow' );
+				} else if ( row === insertAt ) {
+					tableCellToInsert += colspan;
+				}
+			}
 
-					writer.insert( tableRow, table, insertAt );
+			if ( insertAt >= table.childCount ) {
+				tableCellToInsert = columns;
+			}
 
-					for ( let columnIndex = 0; columnIndex < columns; columnIndex++ ) {
-						columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
+			for ( let i = 0; i < rows; i++ ) {
+				const tableRow = writer.createElement( 'tableRow' );
 
-						const cell = writer.createElement( 'tableCell' );
+				writer.insert( tableRow, table, insertAt );
 
-						writer.insert( cell, tableRow, 'end' );
-					}
+				for ( let columnIndex = 0; columnIndex < tableCellToInsert; columnIndex++ ) {
+					const cell = writer.createElement( 'tableCell' );
+
+					writer.insert( cell, tableRow, 'end' );
 				}
 			}
 		} );

+ 63 - 13
packages/ckeditor5-table/src/cellspans.js

@@ -4,15 +4,76 @@
  */
 
 /**
- * @module table/cellspans
+ * @module table/tableiterator
  */
 
+import { getNumericAttribute } from './converters/downcasttable';
+
+export default class TableIterator {
+	constructor( table ) {
+		this.table = table;
+		this.cellSpans = new CellSpans();
+	}
+
+	* iterateOver() {
+		let rowIndex = 0;
+
+		for ( const tableRow of Array.from( this.table.getChildren() ) ) {
+			let columnIndex = 0;
+
+			let i = 0;
+			let tableCell = tableRow.getChild( i );
+
+			while ( tableCell ) {
+				// for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
+				columnIndex = this.cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
+				const colspan = getNumericAttribute( tableCell, 'colspan', 1 );
+				const rowspan = getNumericAttribute( tableCell, 'rowspan', 1 );
+
+				yield {
+					column: columnIndex,
+					row: rowIndex,
+					cell: tableCell,
+					rowspan,
+					colspan
+				};
+
+				// Skip to next "free" column index.
+				const colspanAfter = getNumericAttribute( tableCell, 'colspan', 1 );
+
+				this.cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspanAfter );
+
+				columnIndex += colspanAfter;
+
+				i++;
+				tableCell = tableRow.getChild( i );
+			}
+
+			rowIndex++;
+		}
+	}
+
+	* iterateOverRow( rowIndex ) {
+		for ( const tableCellInfo of this.iterateOver() ) {
+			const { row } = tableCellInfo;
+
+			if ( row === rowIndex ) {
+				yield tableCellInfo;
+			}
+
+			if ( row > rowIndex ) {
+				return;
+			}
+		}
+	}
+}
+
 /**
  * Holds information about spanned table cells.
  *
  * @private
  */
-export default class CellSpans {
+class CellSpans {
 	/**
 	 * Creates CellSpans instance.
 	 */
@@ -95,17 +156,6 @@ export default class CellSpans {
 	}
 
 	/**
-	 * Removes row from mapping.
-	 *
-	 * @param {Number} rowIndex
-	 */
-	drop( rowIndex ) {
-		if ( this._spans.has( rowIndex ) ) {
-			this._spans.delete( rowIndex );
-		}
-	}
-
-	/**
 	 * Checks if given table cell is spanned by other.
 	 *
 	 * @param {Number} rowIndex

+ 0 - 79
packages/ckeditor5-table/tests/cellspans.js

@@ -1,79 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import CellSpans from '../src/cellspans';
-
-describe( 'CellSpans', () => {
-	let cellSpans;
-
-	beforeEach( () => {
-		cellSpans = new CellSpans();
-	} );
-
-	describe( 'recordSpans()', () => {
-		it( 'should record spans relatively to a provided cell index with proper cellspan value', () => {
-			cellSpans.recordSpans( 0, 0, 2, 2 );
-
-			expect( cellSpans._spans.size ).to.equal( 1 );
-			expect( cellSpans._spans.has( 1 ) ).to.be.true;
-			expect( cellSpans._spans.get( 1 ).size ).to.equal( 1 );
-			expect( cellSpans._spans.get( 1 ).get( 0 ) ).to.equal( 2 );
-		} );
-
-		it( 'should record spans for the same row in the same map', () => {
-			cellSpans.recordSpans( 0, 0, 2, 2 );
-			cellSpans.recordSpans( 0, 3, 2, 7 );
-
-			expect( cellSpans._spans.has( 1 ) ).to.be.true;
-			expect( cellSpans._spans.get( 1 ).size ).to.equal( 2 );
-			expect( cellSpans._spans.get( 1 ).get( 3 ) ).to.equal( 7 );
-		} );
-	} );
-
-	describe( 'drop()', () => {
-		it( 'should remove rows', () => {
-			cellSpans.recordSpans( 0, 0, 4, 1 );
-
-			expect( cellSpans._spans.size ).to.equal( 3 );
-			expect( cellSpans._spans.has( 0 ) ).to.be.false;
-			expect( cellSpans._spans.has( 1 ) ).to.be.true;
-			expect( cellSpans._spans.has( 2 ) ).to.be.true;
-			expect( cellSpans._spans.has( 3 ) ).to.be.true;
-			expect( cellSpans._spans.has( 4 ) ).to.be.false;
-
-			cellSpans.drop( 2 );
-
-			expect( cellSpans._spans.size ).to.equal( 2 );
-			expect( cellSpans._spans.has( 0 ) ).to.be.false;
-			expect( cellSpans._spans.has( 1 ) ).to.be.true;
-			expect( cellSpans._spans.has( 2 ) ).to.be.false;
-			expect( cellSpans._spans.has( 3 ) ).to.be.true;
-		} );
-
-		it( 'should do nothing if there was no spans recoreder', () => {
-			cellSpans.recordSpans( 0, 0, 3, 1 );
-
-			expect( cellSpans._spans.size ).to.equal( 2 );
-
-			cellSpans.drop( 1 );
-			expect( cellSpans._spans.size ).to.equal( 1 );
-
-			cellSpans.drop( 1 );
-			expect( cellSpans._spans.size ).to.equal( 1 );
-		} );
-	} );
-
-	describe( 'getNextFreeColumnIndex()', () => {
-		it( 'should return the same column index as provided when no spans recorded', () => {
-			expect( cellSpans.getAdjustedColumnIndex( 1, 1 ) ).to.equal( 1 );
-		} );
-
-		it( 'should return adjusted column index by the size of overlaping rowspan', () => {
-			cellSpans.recordSpans( 0, 1, 2, 8 );
-
-			expect( cellSpans.getAdjustedColumnIndex( 1, 1 ) ).to.equal( 9 );
-		} );
-	} );
-} );

+ 6 - 6
packages/ckeditor5-table/tests/insertcolumncommand.js

@@ -144,17 +144,17 @@ describe( 'InsertColumnCommand', () => {
 
 		it( 'should not update table heading columns attribute when inserting column after headings section', () => {
 			setData( model, modelTable( [
-				[ '11[]', '12' ],
-				[ '21', '22' ],
-				[ '31', '32' ]
+				[ '11[]', '12', '13' ],
+				[ '21', '22', '23' ],
+				[ '31', '32', '33' ]
 			], { headingColumns: 2 } ) );
 
 			command.execute( { at: 2 } );
 
 			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '11[]', '12', '' ],
-				[ '21', '22', '' ],
-				[ '31', '32', '' ]
+				[ '11[]', '12', '', '13' ],
+				[ '21', '22', '', '23' ],
+				[ '31', '32', '', '33' ]
 			], { headingColumns: 2 } ) );
 		} );
 

+ 40 - 4
packages/ckeditor5-table/tests/insertrowcommand.js

@@ -170,20 +170,56 @@ describe( 'InsertRowCommand', () => {
 		it( 'should not expand rowspan of a cell that does not overlaps inserted rows', () => {
 			setData( model, modelTable( [
 				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
-				[ '22', '23', '24' ],
-				[ '33', '34' ]
+				[ '22', '23' ],
+				[ '31', '32', '33' ]
 			], { headingColumns: 3, headingRows: 1 } ) );
 
 			command.execute( { at: 2, rows: 3 } );
 
 			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
-				[ '22', '23', '24' ],
+				[ '22', '23' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
 				[ '', '', '' ],
-				[ '33', '34' ]
+				[ '31', '32', '33' ]
+			], { headingColumns: 3, headingRows: 1 } ) );
+		} );
+
+		it( 'should properly calculate columns if next row has colspans', () => {
+			setData( model, modelTable( [
+				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
+				[ '22', '23' ],
+				[ { colspan: 3, contents: '31' } ]
+			], { headingColumns: 3, headingRows: 1 } ) );
+
+			command.execute( { at: 2, rows: 3 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ { rowspan: 2, contents: '11[]' }, '12', '13' ],
+				[ '22', '23' ],
+				[ '', '', '' ],
+				[ '', '', '' ],
+				[ '', '', '' ],
+				[ { colspan: 3, contents: '31' } ]
 			], { headingColumns: 3, headingRows: 1 } ) );
 		} );
+
+		it( 'should insert rows at the end of a table', () => {
+			setData( model, modelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ]
+			] ) );
+
+			command.execute( { at: 2, rows: 3 } );
+
+			expect( formatModelTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '11[]', '12' ],
+				[ '21', '22' ],
+				[ '', '' ],
+				[ '', '' ],
+				[ '', '' ]
+			] ) );
+		} );
 	} );
 } );

+ 98 - 0
packages/ckeditor5-table/tests/tableiterator.js

@@ -0,0 +1,98 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { modelTable } from './_utils/utils';
+
+import TableIterator from '../src/tableiterator';
+
+describe( 'TableIterator', () => {
+	let editor, model, doc, root;
+
+	beforeEach( () => {
+		return ModelTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+				root = doc.getRoot( 'main' );
+
+				const schema = model.schema;
+
+				schema.register( 'table', {
+					allowWhere: '$block',
+					allowAttributes: [ 'headingRows', 'headingColumns' ],
+					isBlock: true,
+					isObject: true
+				} );
+
+				schema.register( 'tableRow', {
+					allowIn: 'table',
+					allowAttributes: [],
+					isBlock: true,
+					isLimit: true
+				} );
+
+				schema.register( 'tableCell', {
+					allowIn: 'tableRow',
+					allowContentOf: '$block',
+					allowAttributes: [ 'colspan', 'rowspan' ],
+					isBlock: true,
+					isLimit: true
+				} );
+			} );
+	} );
+
+	function testIterator( tableData, expected ) {
+		setData( model, modelTable( tableData ) );
+
+		const iterator = new TableIterator( root.getChild( 0 ) );
+
+		const result = [];
+
+		for ( const tableInfo of iterator.iterateOver() ) {
+			result.push( tableInfo );
+		}
+
+		const formattedResult = result.map( ( { row, column, cell } ) => ( { row, column, data: cell.getChild( 0 ).data } ) );
+		expect( formattedResult ).to.deep.equal( expected );
+	}
+
+	it( 'should iterate over a table', () => {
+		testIterator( [
+			[ '11', '12' ]
+		], [
+			{ row: 0, column: 0, data: '11' },
+			{ row: 0, column: 1, data: '12' }
+		] );
+	} );
+
+	it( 'should properly output column indexes of a table that has colspans', () => {
+		testIterator( [
+			[ { colspan: 2, contents: '11' }, '13' ]
+		], [
+			{ row: 0, column: 0, data: '11' },
+			{ row: 0, column: 2, data: '13' }
+		] );
+	} );
+
+	it( 'should properly output column indexes of a table that has rowspans', () => {
+		testIterator( [
+			[ { colspan: 2, rowspan: 3, contents: '11' }, '13' ],
+			[ '23' ],
+			[ '33' ],
+			[ '41', '42', '43' ]
+		], [
+			{ row: 0, column: 0, data: '11' },
+			{ row: 0, column: 2, data: '13' },
+			{ row: 1, column: 2, data: '23' },
+			{ row: 2, column: 2, data: '33' },
+			{ row: 3, column: 0, data: '41' },
+			{ row: 3, column: 1, data: '42' },
+			{ row: 3, column: 2, data: '43' }
+		] );
+	} );
+} );