Преглед на файлове

Changed: Add custom upcast converter for table.

Maciej Gołaszewski преди 7 години
родител
ревизия
168e0cc056
променени са 3 файла, в които са добавени 224 реда и са изтрити 32 реда
  1. 124 27
      packages/ckeditor5-table/src/converters.js
  2. 97 2
      packages/ckeditor5-table/tests/converters.js
  3. 3 3
      packages/ckeditor5-table/tests/inserttablecommand.js

+ 124 - 27
packages/ckeditor5-table/src/converters.js

@@ -8,10 +8,62 @@
  */
 
 import Position from '@ckeditor/ckeditor5-engine/src/view/position';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 
 export function upcastTable() {
-	return upcastElementToElement( { model: _createModelTable, view: 'table' } );
+	const converter = ( evt, data, conversionApi ) => {
+		const viewTable = data.viewItem;
+
+		// When element was already consumed then skip it.
+		const test = conversionApi.consumable.test( viewTable, { name: true } );
+
+		if ( !test ) {
+			return;
+		}
+
+		const modelTable = conversionApi.writer.createElement( 'table' );
+
+		const splitResult = conversionApi.splitToAllowedParent( modelTable, data.modelCursor );
+
+		// Insert element on allowed position.
+		conversionApi.writer.insert( modelTable, splitResult.position );
+
+		// Convert children and insert to element.
+		// TODO:
+		const childrenResult = _upcastTableRows( viewTable, modelTable, ModelPosition.createAt( modelTable ), conversionApi );
+
+		// Consume appropriate value from consumable values list.
+		conversionApi.consumable.consume( viewTable, { name: true } );
+
+		// Set conversion result range.
+		data.modelRange = new ModelRange(
+			// Range should start before inserted element
+			ModelPosition.createBefore( modelTable ),
+			// Should end after but we need to take into consideration that children could split our
+			// element, so we need to move range after parent of the last converted child.
+			// before: <allowed>[]</allowed>
+			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
+			ModelPosition.createAfter( childrenResult.modelCursor.parent )
+		);
+
+		// Now we need to check where the modelCursor should be.
+		// If we had to split parent to insert our element then we want to continue conversion inside split parent.
+		//
+		// before: <allowed><notAllowed>[]</notAllowed></allowed>
+		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
+		if ( splitResult.cursorParent ) {
+			data.modelCursor = ModelPosition.createAt( splitResult.cursorParent );
+
+			// Otherwise just continue after inserted element.
+		} else {
+			data.modelCursor = data.modelRange.end;
+		}
+	};
+
+	return dispatcher => {
+		dispatcher.on( 'element:table', converter, { priority: 'normal' } );
+	};
 }
 
 export function downcastTableCell() {
@@ -46,11 +98,11 @@ export function downcastTable() {
 		const bodyRows = tableRows.slice( headingRows );
 
 		if ( headingRows ) {
-			_createTableSection( 'thead', tableElement, headings, conversionApi );
+			_downcastTableSection( 'thead', tableElement, headings, conversionApi );
 		}
 
 		if ( bodyRows.length ) {
-			_createTableSection( 'tbody', tableElement, bodyRows, conversionApi );
+			_downcastTableSection( 'tbody', tableElement, bodyRows, conversionApi );
 		}
 
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
@@ -60,29 +112,7 @@ export function downcastTable() {
 	}, { priority: 'normal' } );
 }
 
-export function _createModelTable( viewElement, modelWriter ) {
-	const attributes = {};
-
-	const header = _getChildHeader( viewElement );
-
-	if ( header ) {
-		attributes.headingRows = header.childCount;
-	}
-
-	return modelWriter.createElement( 'table', attributes );
-}
-
-function _getChildHeader( table ) {
-	for ( const child of Array.from( table.getChildren() ) ) {
-		if ( child.name === 'thead' ) {
-			return child;
-		}
-	}
-
-	return false;
-}
-
-function _createTableSection( elementName, tableElement, rows, conversionApi ) {
+function _downcastTableSection( elementName, tableElement, rows, conversionApi ) {
 	const tableBodyElement = conversionApi.writer.createContainerElement( elementName );
 	conversionApi.writer.insert( Position.createAt( tableElement, 'end' ), tableBodyElement );
 
@@ -98,3 +128,70 @@ function _downcastTableRow( tableRow, conversionApi, parent ) {
 	conversionApi.mapper.bindElements( tableRow, tableRowElement );
 	conversionApi.writer.insert( Position.createAt( parent, 'end' ), tableRowElement );
 }
+
+function _sortRows( table, conversionApi ) {
+	const rows = { header: [], body: [] };
+
+	let firstThead;
+
+	for ( const viewChild of Array.from( table.getChildren() ) ) {
+		if ( viewChild.name === 'tbody' || viewChild.name === 'thead' || viewChild.name === 'tfoot' ) {
+			if ( viewChild.name === 'thead' && !firstThead ) {
+				firstThead = viewChild;
+			}
+
+			for ( const childRow of Array.from( viewChild.getChildren() ) ) {
+				_createModelRow( childRow, rows, conversionApi, firstThead );
+			}
+		}
+	}
+
+	return rows;
+}
+
+function _upcastTableRows( table, modelTable, modelCursor, conversionApi ) {
+	const modelRange = new ModelRange( modelCursor );
+
+	const rows = _sortRows( table, conversionApi, modelCursor );
+
+	const allRows = [ ...rows.header, ...rows.body ];
+
+	for ( const rowDef of allRows ) {
+		const rowPosition = ModelPosition.createAt( modelTable, 'end' );
+
+		conversionApi.writer.insert( rowDef.model, rowPosition );
+		conversionApi.consumable.consume( rowDef.view, { name: true } );
+
+		const childrenCursor = ModelPosition.createAt( rowDef.model );
+		conversionApi.convertChildren( rowDef.view, childrenCursor );
+	}
+
+	if ( rows.header.length ) {
+		conversionApi.writer.setAttribute( 'headingRows', rows.header.length, modelTable );
+	}
+
+	if ( !allRows.length ) {
+		const rowPosition = ModelPosition.createAt( modelTable, 'end' );
+
+		const row = conversionApi.writer.createElement( 'tableRow' );
+
+		conversionApi.writer.insert( row, rowPosition );
+
+		const emptyCell = conversionApi.writer.createElement( 'tableCell' );
+
+		conversionApi.writer.insert( emptyCell, ModelPosition.createAt( row, 'end' ) );
+	}
+
+	return { modelRange, modelCursor };
+}
+
+function _createModelRow( row, rows, conversionApi, firstThead ) {
+	const modelRow = conversionApi.writer.createElement( 'tableRow' );
+
+	if ( row.parent.name === 'thead' && row.parent === firstThead ) {
+		rows.header.push( { model: modelRow, view: row } );
+	} else {
+		rows.body.push( { model: modelRow, view: row } );
+	}
+}
+

+ 97 - 2
packages/ckeditor5-table/tests/converters.js

@@ -122,7 +122,8 @@ describe( 'Table converters', () => {
 			editor.setData(
 				'<table>' +
 				'<thead><tr><td>1</td></tr></thead>' +
-				'<thead><tr><td>2</td></tr></thead>' +
+				'<tbody><tr><td>2</td></tr></tbody>' +
+				'<thead><tr><td>3</td></tr></thead>' +
 				'</table>'
 			);
 
@@ -130,11 +131,12 @@ describe( 'Table converters', () => {
 				'<table headingRows="1">' +
 				'<tableRow><tableCell>1</tableCell></tableRow>' +
 				'<tableRow><tableCell>2</tableCell></tableRow>' +
+				'<tableRow><tableCell>3</tableCell></tableRow>' +
 				'</table>'
 			);
 		} );
 
-		it.skip( 'should create table model from table with thead after the tbody', () => {
+		it( 'should create table model from table with thead after the tbody', () => {
 			editor.setData(
 				'<table>' +
 				'<tbody><tr><td>2</td></tr></tbody>' +
@@ -149,6 +151,99 @@ describe( 'Table converters', () => {
 				'</table>'
 			);
 		} );
+
+		it( 'should create table model from table with one tfoot with one row', () => {
+			editor.setData(
+				'<table>' +
+				'<tfoot><tr><td>1</td></tr></tfoot>' +
+				'</table>'
+			);
+
+			expectModel(
+				'<table>' +
+				'<tableRow><tableCell>1</tableCell></tableRow>' +
+				'</table>'
+			);
+		} );
+
+		it( 'should create valid table model from empty table', () => {
+			editor.setData(
+				'<table>' +
+				'</table>'
+			);
+
+			expectModel(
+				'<table><tableRow><tableCell></tableCell></tableRow></table>'
+			);
+		} );
+
+		it( 'should skip unknown table children', () => {
+			editor.setData(
+				'<table>' +
+				'<caption>foo</caption>' +
+				'<tr><td>bar</td></tr>' +
+				'</table>'
+			);
+
+			expectModel(
+				'<table><tableRow><tableCell>bar</tableCell></tableRow></table>'
+			);
+		} );
+
+		it( 'should create table model from some broken table', () => {
+			editor.setData(
+				'<table><td><p>foo</p></td></table>'
+			);
+
+			expectModel(
+				'<table><tableRow><tableCell>foo</tableCell></tableRow></table>'
+			);
+		} );
+
+		it( 'should fix if inside other blocks', () => {
+			editor.model.schema.register( 'p', {
+				inheritAllFrom: '$block'
+			} );
+			editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'p', view: 'p' } ) );
+
+			editor.setData(
+				'<p>foo' +
+				'<table>' +
+				'<tbody><tr><td>2</td></tr></tbody>' +
+				'<thead><tr><td>1</td></tr></thead>' +
+				'</table>' +
+				'</p>'
+			);
+
+			expectModel(
+				'<p>foo</p>' +
+				'<table headingRows="1">' +
+				'<tableRow><tableCell>1</tableCell></tableRow>' +
+				'<tableRow><tableCell>2</tableCell></tableRow>' +
+				'</table>'
+			);
+		} );
+
+		it( 'should be possible to overwrite table conversion', () => {
+			editor.model.schema.register( 'fooTable', {
+				allowWhere: '$block',
+				allowAttributes: [ 'headingRows' ],
+				isBlock: true,
+				isObject: true
+			} );
+
+			editor.conversion.elementToElement( { model: 'fooTable', view: 'table', priority: 'high' } );
+
+			editor.setData(
+				'<table>' +
+				'<thead><tr><td>foo</td></tr></thead>' +
+				'</table>'
+			);
+
+			expectModel(
+				'<fooTable></fooTable>'
+			);
+		} );
 	} );
 
 	describe( 'downcastTable()', () => {

+ 3 - 3
packages/ckeditor5-table/tests/inserttablecommand.js

@@ -7,7 +7,7 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltestedit
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 import InsertTableCommand from '../src/inserttablecommand';
-import { _createModelTable, downcastTable, downcastTableCell } from '../src/converters';
+import { upcastTable, downcastTable, downcastTableCell } from '../src/converters';
 
 describe( 'InsertTableCommand', () => {
 	let editor, model, command;
@@ -47,8 +47,8 @@ describe( 'InsertTableCommand', () => {
 				model.schema.register( 'p', { inheritAllFrom: '$block' } );
 
 				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: _createModelTable, view: 'table' } ) );
-				conversion.for( 'downcast' ).add( downcastTable );
+				conversion.for( 'upcast' ).add( upcastTable() );
+				conversion.for( 'downcast' ).add( downcastTable() );
 
 				// Table row upcast only since downcast conversion is done in `downcastTable()`.
 				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );