Răsfoiți Sursa

Changed: Extract CellSpans to own file as it is used by commands and converters.

Maciej Gołaszewski 7 ani în urmă
părinte
comite
1d0fa001b6

+ 125 - 0
packages/ckeditor5-table/src/cellspans.js

@@ -0,0 +1,125 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/cellspans
+ */
+
+/**
+ * Holds information about spanned table cells.
+ *
+ * @private
+ */
+export default class CellSpans {
+	/**
+	 * Creates CellSpans instance.
+	 */
+	constructor() {
+		/**
+		 * Holds table cell spans mapping.
+		 *
+		 * @type {Map<Number, Number>}
+		 * @private
+		 */
+		this._spans = new Map();
+	}
+
+	/**
+	 * Returns proper column index if a current cell index is overlapped by other (has a span defined).
+	 *
+	 * @param {Number} row
+	 * @param {Number} column
+	 * @return {Number} Returns current column or updated column index.
+	 */
+	getAdjustedColumnIndex( row, column ) {
+		let span = this._check( row, column ) || 0;
+
+		// Offset current table cell columnIndex by spanning cells from rows above.
+		while ( span ) {
+			column += span;
+			span = this._check( row, column );
+		}
+
+		return column;
+	}
+
+	/**
+	 * Updates spans based on current table cell height & width. Spans with height <= 1 will not be recorded.
+	 *
+	 * For instance if a table cell at row 0 and column 0 has height of 3 and width of 2 we're setting spans:
+	 *
+	 *        0 1 2 3 4 5
+	 *     0:
+	 *     1: 2
+	 *     2: 2
+	 *     3:
+	 *
+	 * Adding another spans for a table cell at row 2 and column 1 that has height of 2 and width of 4 will update above to:
+	 *
+	 *        0 1 2 3 4 5
+	 *     0:
+	 *     1: 2
+	 *     2: 2
+	 *     3:   4
+	 *
+	 * The above span mapping was calculated from a table below (cells 03 & 12 were not added as their height is 1):
+	 *
+	 *     +----+----+----+----+----+----+
+	 *     | 00      | 02 | 03      | 05 |
+	 *     |         +--- +----+----+----+
+	 *     |         | 12      | 24 | 25 |
+	 *     |         +----+----+----+----+
+	 *     |         | 22                |
+	 *     |----+----+                   +
+	 *     | 31 | 32 |                   |
+	 *     +----+----+----+----+----+----+
+	 *
+	 * @param {Number} rowIndex
+	 * @param {Number} columnIndex
+	 * @param {Number} height
+	 * @param {Number} width
+	 */
+	recordSpans( rowIndex, columnIndex, height, width ) {
+		// This will update all rows below up to row height with value of span width.
+		for ( let rowToUpdate = rowIndex + 1; rowToUpdate < rowIndex + height; rowToUpdate++ ) {
+			if ( !this._spans.has( rowToUpdate ) ) {
+				this._spans.set( rowToUpdate, new Map() );
+			}
+
+			const rowSpans = this._spans.get( rowToUpdate );
+
+			rowSpans.set( columnIndex, width );
+		}
+	}
+
+	/**
+	 * 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
+	 * @param {Number} columnIndex
+	 * @return {Boolean|Number} Returns false or width of a span.
+	 * @private
+	 */
+	_check( rowIndex, columnIndex ) {
+		if ( !this._spans.has( rowIndex ) ) {
+			return false;
+		}
+
+		const rowSpans = this._spans.get( rowIndex );
+
+		return rowSpans.has( columnIndex ) ? rowSpans.get( columnIndex ) : false;
+	}
+}

+ 18 - 128
packages/ckeditor5-table/src/converters/downcasttable.js

@@ -9,6 +9,7 @@
 
 import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
+import CellSpans from '../cellspans';
 
 /**
  * Model table element to view table element conversion helper.
@@ -36,7 +37,7 @@ export default function downcastTable() {
 		const headingRows = getNumericAttribute( table, 'headingRows', 0 );
 		const tableRows = Array.from( table.getChildren() );
 
-		const cellSpans = createPreviousCellSpans( table, 0 );
+		const cellSpans = getCellSpansWithPreviousFilled( table, 0 );
 
 		for ( const tableRow of tableRows ) {
 			const rowIndex = tableRows.indexOf( tableRow );
@@ -84,7 +85,7 @@ export function downcastInsertRow() {
 		const tableSection = Array.from( tableElement.getChildren() )
 			.filter( child => child.name === ( isHeadingRow ? 'thead' : 'tbody' ) )[ 0 ];
 
-		const cellSpans = createPreviousCellSpans( table, rowIndex );
+		const cellSpans = getCellSpansWithPreviousFilled( table, rowIndex );
 
 		downcastTableRow( tableRow, rowIndex, tableSection, cellSpans, conversionApi );
 	}, { priority: 'normal' } );
@@ -119,13 +120,13 @@ export function downcastInsertCell() {
 
 		let columnIndex = 0;
 
-		const cellSpans = createPreviousCellSpans( table, rowIndex, columnIndex );
+		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.getNextFreeColumnIndex( rowIndex, columnIndex );
+		columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 		const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
 
@@ -157,7 +158,7 @@ export function downcastAttributeChange( attribute ) {
 
 		const tableRows = Array.from( table.getChildren() );
 
-		const cellSpans = createPreviousCellSpans( table, 0 );
+		const cellSpans = getCellSpansWithPreviousFilled( table, 0 );
 
 		const cachedTableSections = {};
 
@@ -194,7 +195,7 @@ export function downcastAttributeChange( attribute ) {
 
 			for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
 				// Check whether current columnIndex is overlapped by table cells from previous rows.
-				columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
+				columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 				const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
 
@@ -226,7 +227,7 @@ export function downcastAttributeChange( attribute ) {
 //
 // @param {module:engine/model/element~Element} tableRow
 // @param {Number} rowIndex
-// @param {CellSpans} cellSpans
+// @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 );
@@ -272,7 +273,7 @@ function downcastTableRow( tableRow, rowIndex, tableSection, cellSpans, conversi
 
 	for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
 		// Check whether current columnIndex is overlapped by table cells from previous rows.
-		columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
+		columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 		const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
 
@@ -376,7 +377,7 @@ export function getNumericAttribute( element, 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.getNextFreeColumnIndex( rowIndex, columnIndex );
+		columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 		// Up to here only!
 		if ( tableCellA === tableCell ) {
@@ -393,7 +394,12 @@ function getColumnIndex( tableRow, columnIndex, cellSpans, rowIndex, tableCell )
 	}
 }
 
-function createPreviousCellSpans( table, currentRowIndex ) {
+// 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++ ) {
@@ -401,8 +407,9 @@ function createPreviousCellSpans( table, currentRowIndex ) {
 
 		let columnIndex = 0;
 
+		// TODO: make this an iterator?
 		for ( const tableCell of Array.from( row.getChildren() ) ) {
-			columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
+			columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 			const colspan = getNumericAttribute( tableCell, 'colspan', 1 );
 			const rowspan = getNumericAttribute( tableCell, 'rowspan', 1 );
@@ -416,120 +423,3 @@ function createPreviousCellSpans( table, currentRowIndex ) {
 
 	return cellSpans;
 }
-
-/**
- * Holds information about spanned table cells.
- *
- * @private
- */
-export class CellSpans {
-	/**
-	 * Creates CellSpans instance.
-	 */
-	constructor() {
-		/**
-		 * Holds table cell spans mapping.
-		 *
-		 * @type {Map<Number, Number>}
-		 * @private
-		 */
-		this._spans = new Map();
-	}
-
-	/**
-	 * Returns proper column index if a current cell index is overlapped by other (has a span defined).
-	 *
-	 * @param {Number} row
-	 * @param {Number} column
-	 * @return {Number} Returns current column or updated column index.
-	 */
-	getNextFreeColumnIndex( row, column ) {
-		let span = this._check( row, column ) || 0;
-
-		// Offset current table cell columnIndex by spanning cells from rows above.
-		while ( span ) {
-			column += span;
-			span = this._check( row, column );
-		}
-
-		return column;
-	}
-
-	/**
-	 * Updates spans based on current table cell height & width. Spans with height <= 1 will not be recorded.
-	 *
-	 * For instance if a table cell at row 0 and column 0 has height of 3 and width of 2 we're setting spans:
-	 *
-	 *        0 1 2 3 4 5
-	 *     0:
-	 *     1: 2
-	 *     2: 2
-	 *     3:
-	 *
-	 * Adding another spans for a table cell at row 2 and column 1 that has height of 2 and width of 4 will update above to:
-	 *
-	 *        0 1 2 3 4 5
-	 *     0:
-	 *     1: 2
-	 *     2: 2
-	 *     3:   4
-	 *
-	 * The above span mapping was calculated from a table below (cells 03 & 12 were not added as their height is 1):
-	 *
-	 *     +----+----+----+----+----+----+
-	 *     | 00      | 02 | 03      | 05 |
-	 *     |         +--- +----+----+----+
-	 *     |         | 12      | 24 | 25 |
-	 *     |         +----+----+----+----+
-	 *     |         | 22                |
-	 *     |----+----+                   +
-	 *     | 31 | 32 |                   |
-	 *     +----+----+----+----+----+----+
-	 *
-	 * @param {Number} rowIndex
-	 * @param {Number} columnIndex
-	 * @param {Number} height
-	 * @param {Number} width
-	 */
-	recordSpans( rowIndex, columnIndex, height, width ) {
-		// This will update all rows below up to row height with value of span width.
-		for ( let rowToUpdate = rowIndex + 1; rowToUpdate < rowIndex + height; rowToUpdate++ ) {
-			if ( !this._spans.has( rowToUpdate ) ) {
-				this._spans.set( rowToUpdate, new Map() );
-			}
-
-			const rowSpans = this._spans.get( rowToUpdate );
-
-			rowSpans.set( columnIndex, width );
-		}
-	}
-
-	/**
-	 * 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
-	 * @param {Number} columnIndex
-	 * @return {Boolean|Number} Returns false or width of a span.
-	 * @private
-	 */
-	_check( rowIndex, columnIndex ) {
-		if ( !this._spans.has( rowIndex ) ) {
-			return false;
-		}
-
-		const rowSpans = this._spans.get( rowIndex );
-
-		return rowSpans.has( columnIndex ) ? rowSpans.get( columnIndex ) : false;
-	}
-}

+ 2 - 2
packages/ckeditor5-table/src/insertcolumncommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { CellSpans } from './converters/downcasttable';
+import CellSpans from './cellspans';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 
 /**
@@ -71,7 +71,7 @@ export default class InsertColumnCommand extends Command {
 					let colspan = tableCell.hasAttribute( 'colspan' ) ? parseInt( tableCell.getAttribute( 'colspan' ) ) : 1;
 					const rowspan = tableCell.hasAttribute( 'rowspan' ) ? parseInt( tableCell.getAttribute( 'rowspan' ) ) : 1;
 
-					columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
+					columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 					// TODO: this is not cool:
 					const shouldExpandSpan = colspan > 1 &&

+ 4 - 3
packages/ckeditor5-table/src/insertrowcommand.js

@@ -8,7 +8,8 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { CellSpans, getNumericAttribute } from './converters/downcasttable';
+import { getNumericAttribute } from './converters/downcasttable';
+import CellSpans from './cellspans';
 
 /**
  * The insert row command.
@@ -68,7 +69,7 @@ export default class InsertRowCommand extends Command {
 					let columnIndex = 0;
 
 					for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
-						columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
+						columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 						const colspan = getNumericAttribute( tableCell, 'colspan', 1 );
 						let rowspan = getNumericAttribute( tableCell, 'rowspan', 1 );
@@ -93,7 +94,7 @@ export default class InsertRowCommand extends Command {
 					writer.insert( tableRow, table, insertAt );
 
 					for ( let columnIndex = 0; columnIndex < columns; columnIndex++ ) {
-						columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
+						columnIndex = cellSpans.getAdjustedColumnIndex( rowIndex, columnIndex );
 
 						const cell = writer.createElement( 'tableCell' );
 

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

@@ -0,0 +1,79 @@
+/**
+ * @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 );
+		} );
+	} );
+} );