瀏覽代碼

Added: Calculate heading column cells should consider rowspan and cellspan attributes.

Maciej Gołaszewski 7 年之前
父節點
當前提交
da984b2c49
共有 2 個文件被更改,包括 164 次插入38 次删除
  1. 90 37
      packages/ckeditor5-table/src/converters.js
  2. 74 1
      packages/ckeditor5-table/tests/converters.js

+ 90 - 37
packages/ckeditor5-table/src/converters.js

@@ -74,19 +74,27 @@ export function downcastTable() {
 		}
 
 		const tableElement = conversionApi.writer.createContainerElement( 'table' );
+		const headingRowsCount = table.getAttribute( 'headingRows' ) || 0;
+		const tableRows = Array.from( table.getChildren() );
+		const cellSpans = new CellSpans();
 
-		const headingRows = table.getAttribute( 'headingRows' );
+		const tHead = headingRowsCount ? _createTableSection( 'thead', tableElement, conversionApi ) : undefined;
+		const tBody = headingRowsCount < tableRows.length ? _createTableSection( 'tbody', tableElement, conversionApi ) : undefined;
 
-		const tableRows = [ ...table.getChildren() ];
-		const headings = tableRows.slice( 0, headingRows );
-		const bodyRows = tableRows.slice( headingRows );
+		let parent;
 
-		if ( headingRows ) {
-			_downcastTableSection( 'thead', tableElement, headings, conversionApi );
-		}
+		for ( let rowIndex = 0; rowIndex < tableRows.length; rowIndex++ ) {
+			if ( headingRowsCount && rowIndex < headingRowsCount ) {
+				parent = tHead;
+			} else {
+				parent = tBody;
+			}
+
+			const row = tableRows[ rowIndex ];
+
+			_downcastTableRow( row, rowIndex, cellSpans, parent, conversionApi );
 
-		if ( bodyRows.length ) {
-			_downcastTableSection( 'tbody', tableElement, bodyRows, conversionApi );
+			cellSpans.drop( rowIndex );
 		}
 
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
@@ -96,54 +104,65 @@ export function downcastTable() {
 	}, { priority: 'normal' } );
 }
 
-function isHead( tableCell, headingColumnsLeft ) {
-	const row = tableCell.parent;
-	const table = row.parent;
-	const rowIndex = table.getChildIndex( row );
-	const headingRows = table.getAttribute( 'headingRows' ) || 0;
-	const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
-
-	const cellIndex = row.getChildIndex( tableCell );
-
-	return ( !!headingRows && headingRows > rowIndex ) || ( !!headingColumnsLeft && headingColumns > cellIndex );
-}
-
-function _downcastTableSection( elementName, tableElement, rows, conversionApi ) {
-	const tableBodyElement = conversionApi.writer.createContainerElement( elementName );
-	conversionApi.writer.insert( Position.createAt( tableElement, 'end' ), tableBodyElement );
-
-	rows.map( row => _downcastTableRow( row, conversionApi, tableBodyElement ) );
-}
-
-function _downcastTableRow( tableRow, conversionApi, parent ) {
+function _downcastTableRow( tableRow, rowIndex, cellSpans, parent, conversionApi ) {
 	// Will always consume since we're converting <tableRow> element from a parent <table>.
 	conversionApi.consumable.consume( tableRow, 'insert' );
-
 	const trElement = conversionApi.writer.createContainerElement( 'tr' );
 
 	conversionApi.mapper.bindElements( tableRow, trElement );
 	conversionApi.writer.insert( Position.createAt( parent, 'end' ), trElement );
 
-	let headingColumnsLeft = tableRow.parent.getAttribute( 'headingColumns' ) || 0;
+	let cellIndex = 0;
+
+	const headingColumns = tableRow.parent.getAttribute( 'headingColumns' ) || 0;
 
 	for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
-		// Will always consume since we're converting <tableRow> element from a parent <table>.
-		conversionApi.consumable.consume( tableCell, 'insert' );
+		let shift = cellSpans.check( rowIndex, cellIndex ) || 0;
+
+		while ( shift ) {
+			cellIndex += shift;
+			shift = cellSpans.check( rowIndex, cellIndex );
+		}
 
-		const is = isHead( tableCell, headingColumnsLeft );
+		const cellWidth = tableCell.hasAttribute( 'colspan' ) ? parseInt( tableCell.getAttribute( 'colspan' ) ) : 1;
+		const cellHeight = tableCell.hasAttribute( 'rowspan' ) ? parseInt( tableCell.getAttribute( 'rowspan' ) ) : 1;
 
-		if ( headingColumnsLeft ) {
-			headingColumnsLeft -= tableCell.hasAttribute( 'colspan' ) ? tableCell.getAttribute( 'colspan' ) : 1;
+		if ( cellHeight > 1 ) {
+			cellSpans.update( rowIndex, cellIndex, cellHeight, cellWidth );
 		}
 
-		const tableCellElement = conversionApi.writer.createContainerElement( is ? 'th' : 'td' );
+		// Will always consume since we're converting <tableRow> element from a parent <table>.
+		conversionApi.consumable.consume( tableCell, 'insert' );
+
+		const isHead = _isHead( tableCell, cellIndex, headingColumns );
+
+		const tableCellElement = conversionApi.writer.createContainerElement( isHead ? 'th' : 'td' );
 		const viewPosition = Position.createAt( trElement, 'end' );
 
 		conversionApi.mapper.bindElements( tableCell, tableCellElement );
 		conversionApi.writer.insert( viewPosition, tableCellElement );
+
+		cellIndex += cellWidth;
 	}
 }
 
+function _isHead( tableCell, cellIndex, columnHeadings ) {
+	const row = tableCell.parent;
+	const table = row.parent;
+	const rowIndex = table.getChildIndex( row );
+	const headingRows = table.getAttribute( 'headingRows' ) || 0;
+
+	return ( !!headingRows && headingRows > rowIndex ) || ( !!columnHeadings && columnHeadings > cellIndex );
+}
+
+function _createTableSection( elementName, tableElement, conversionApi ) {
+	const tableChildElement = conversionApi.writer.createContainerElement( elementName );
+
+	conversionApi.writer.insert( Position.createAt( tableElement, 'end' ), tableChildElement );
+
+	return tableChildElement;
+}
+
 function _upcastTableRows( viewTable, modelTable, conversionApi ) {
 	const { rows, headingRows, headingColumns } = _scanTable( viewTable );
 
@@ -251,3 +270,37 @@ function _scanRow( tr, tableMeta, firstThead ) {
 		tableMeta.headingColumns = headingCols;
 	}
 }
+
+export class CellSpans {
+	constructor() {
+		this._spans = new Map();
+	}
+
+	check( row, column ) {
+		if ( !this._spans.has( row ) ) {
+			return false;
+		}
+
+		const rowSpans = this._spans.get( row );
+
+		return rowSpans.has( column ) ? rowSpans.get( column ) : false;
+	}
+
+	update( row, column, height, width ) {
+		if ( height > 1 ) {
+			for ( let nextRow = row + 1; nextRow < row + height; nextRow++ ) {
+				const rowSpans = this._spans.has( nextRow ) ? this._spans.get( nextRow ) : new Map();
+
+				rowSpans.set( column, width );
+
+				this._spans.set( nextRow, rowSpans );
+			}
+		}
+	}
+
+	drop( row ) {
+		if ( this._spans.has( row ) ) {
+			this._spans.delete( row );
+		}
+	}
+}

+ 74 - 1
packages/ckeditor5-table/tests/converters.js

@@ -8,7 +8,7 @@ import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversio
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { downcastTable, upcastTable } from '../src/converters';
+import { downcastTable, CellSpans, upcastTable } from '../src/converters';
 
 describe( 'Table converters', () => {
 	let editor, model, viewDocument;
@@ -467,6 +467,79 @@ describe( 'Table converters', () => {
 					'</table>'
 				);
 			} );
+
+			it( 'should work with colspan and rowspan attributes on table cells', () => {
+				// The table in this test looks like a table below:
+				//
+				//   Row headings | Normal cells
+				//                |
+				// +----+----+----+----+
+				// | 11 | 12 | 13 | 14 |
+				// |----+----+    +----+
+				// | 21 | 22 |    | 24 |
+				// |----+----+    +----+
+				// | 31      |    | 34 |
+				// |         +----+----+
+				// |         | 43 | 44 |
+				// +----+----+----+----+
+
+				setModelData( model,
+					'<table headingColumns="3">' +
+					'<tableRow>' +
+					'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell rowspan="3">13</tableCell><tableCell>14</tableCell>' +
+					'</tableRow>' +
+					'<tableRow><tableCell>21</tableCell><tableCell>22</tableCell><tableCell>24</tableCell></tableRow>' +
+					'<tableRow><tableCell colspan="2" rowspan="2">31</tableCell><tableCell>34</tableCell></tableRow>' +
+					'<tableRow><tableCell>43</tableCell><tableCell>44</tableCell></tableRow>' +
+					'</table>'
+				);
+
+				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+					'<table>' +
+					'<tbody>' +
+					'<tr><th>11</th><th>12</th><th rowspan="3">13</th><td>14</td></tr>' +
+					'<tr><th>21</th><th>22</th><td>24</td></tr>' +
+					'<tr><th colspan="2" rowspan="2">31</th><td>34</td></tr>' +
+					'<tr><th>43</th><td>44</td></tr>' +
+					'</tbody>' +
+					'</table>'
+				);
+			} );
+		} );
+	} );
+
+	describe( 'spanner', () => {
+		let spanner;
+
+		beforeEach( () => {
+			spanner = new CellSpans();
+		} );
+
+		it( 'should work', () => {
+			expect( spanner.check( 2, 1 ) ).to.be.false;
+
+			spanner.update( 1, 1, 3, 2 );
+
+			expect( spanner.check( 2, 0 ) ).to.be.false;
+			expect( spanner.check( 2, 1 ) ).to.equal( 2 );
+			expect( spanner.check( 3, 1 ) ).to.equal( 2 );
+
+			spanner.drop( 2 );
+
+			expect( spanner.check( 2, 1 ) ).to.be.false;
+			expect( spanner.check( 3, 1 ) ).to.equal( 2 );
+
+			spanner.drop( 2 );
+
+			expect( spanner.check( 2, 1 ) ).to.be.false;
+			expect( spanner.check( 3, 1 ) ).to.equal( 2 );
+
+			spanner.update( 2, 4, 2, 3 );
+			expect( spanner.check( 3, 1 ) ).to.equal( 2 );
+			expect( spanner.check( 3, 4 ) ).to.equal( 3 );
+
+			spanner.update( 1, 1, 1, 5 );
+			expect( spanner.check( 1, 1 ) ).to.be.false;
 		} );
 	} );
 } );