Przeglądaj źródła

Merge pull request #97 from ckeditor/t/56

Feature: Support block content inside table. Closes #56.

BREAKING CHANGE: Removed `table/commands/utils~getParentTable()` method. Use `table/commands/utils~findAncestor()` instead.
Oskar Wróbel 7 lat temu
rodzic
commit
8d76347e41
46 zmienionych plików z 2016 dodań i 1068 usunięć
  1. 6 1
      packages/ckeditor5-table/package.json
  2. 6 4
      packages/ckeditor5-table/src/commands/insertcolumncommand.js
  3. 6 5
      packages/ckeditor5-table/src/commands/insertrowcommand.js
  4. 1 1
      packages/ckeditor5-table/src/commands/inserttablecommand.js
  5. 36 10
      packages/ckeditor5-table/src/commands/mergecellcommand.js
  6. 4 4
      packages/ckeditor5-table/src/commands/removecolumncommand.js
  7. 4 4
      packages/ckeditor5-table/src/commands/removerowcommand.js
  8. 5 5
      packages/ckeditor5-table/src/commands/setheadercolumncommand.js
  9. 7 7
      packages/ckeditor5-table/src/commands/setheaderrowcommand.js
  10. 4 3
      packages/ckeditor5-table/src/commands/splitcellcommand.js
  11. 18 4
      packages/ckeditor5-table/src/commands/utils.js
  12. 30 6
      packages/ckeditor5-table/src/converters/downcast.js
  13. 4 4
      packages/ckeditor5-table/src/converters/table-post-fixer.js
  14. 175 0
      packages/ckeditor5-table/src/converters/tablecell-post-fixer.js
  15. 59 3
      packages/ckeditor5-table/src/converters/upcasttable.js
  16. 77 71
      packages/ckeditor5-table/src/tableediting.js
  17. 8 6
      packages/ckeditor5-table/src/tableutils.js
  18. 2 2
      packages/ckeditor5-table/src/ui/utils.js
  19. 3 3
      packages/ckeditor5-table/src/utils.js
  20. 128 12
      packages/ckeditor5-table/tests/_utils/utils.js
  21. 32 58
      packages/ckeditor5-table/tests/commands/insertcolumncommand.js
  22. 40 56
      packages/ckeditor5-table/tests/commands/insertrowcommand.js
  23. 11 61
      packages/ckeditor5-table/tests/commands/inserttablecommand.js
  24. 295 73
      packages/ckeditor5-table/tests/commands/mergecellcommand.js
  25. 9 59
      packages/ckeditor5-table/tests/commands/removecolumncommand.js
  26. 9 59
      packages/ckeditor5-table/tests/commands/removerowcommand.js
  27. 7 64
      packages/ckeditor5-table/tests/commands/setheadercolumncommand.js
  28. 7 58
      packages/ckeditor5-table/tests/commands/setheaderrowcommand.js
  29. 6 56
      packages/ckeditor5-table/tests/commands/splitcellcommand.js
  30. 8 41
      packages/ckeditor5-table/tests/commands/utils.js
  31. 143 178
      packages/ckeditor5-table/tests/converters/downcast.js
  32. 5 2
      packages/ckeditor5-table/tests/converters/table-post-fixer.js
  33. 211 0
      packages/ckeditor5-table/tests/converters/tablecell-post-fixer.js
  34. 171 61
      packages/ckeditor5-table/tests/converters/upcasttable.js
  35. 12 3
      packages/ckeditor5-table/tests/integration.js
  36. BIN
      packages/ckeditor5-table/tests/manual/sample.jpg
  37. 4 2
      packages/ckeditor5-table/tests/manual/table.html
  38. 69 0
      packages/ckeditor5-table/tests/manual/tableblockcontent.html
  39. 30 0
      packages/ckeditor5-table/tests/manual/tableblockcontent.js
  40. 19 0
      packages/ckeditor5-table/tests/manual/tableblockcontent.md
  41. 151 0
      packages/ckeditor5-table/tests/table-integration.js
  42. 174 5
      packages/ckeditor5-table/tests/tableediting.js
  43. 9 3
      packages/ckeditor5-table/tests/tabletoolbar.js
  44. 6 55
      packages/ckeditor5-table/tests/tableutils.js
  45. 5 18
      packages/ckeditor5-table/tests/tablewalker.js
  46. 0 1
      packages/ckeditor5-table/theme/table.css

+ 6 - 1
packages/ckeditor5-table/package.json

@@ -15,9 +15,14 @@
     "@ckeditor/ckeditor5-widget": "^10.2.0"
   },
   "devDependencies": {
+    "@ckeditor/ckeditor5-alignment": "^10.0.2",
+    "@ckeditor/ckeditor5-block-quote": "^10.0.2",
+    "@ckeditor/ckeditor5-clipboard": "^10.0.2",
     "@ckeditor/ckeditor5-editor-classic": "^11.0.0",
+    "@ckeditor/ckeditor5-image": "^10.2.0",
+    "@ckeditor/ckeditor5-list": "^11.0.1",
     "@ckeditor/ckeditor5-paragraph": "^10.0.2",
-    "@ckeditor/ckeditor5-undo": "^10.0.1",
+    "@ckeditor/ckeditor5-undo": "^10.0.2",
     "@ckeditor/ckeditor5-utils": "^10.2.0",
     "eslint": "^4.15.0",
     "eslint-config-ckeditor5": "^1.0.7",

+ 6 - 4
packages/ckeditor5-table/src/commands/insertcolumncommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getParentTable } from './utils';
+import { findAncestor } from './utils';
 import TableUtils from '../tableutils';
 
 /**
@@ -54,7 +54,7 @@ export default class InsertColumnCommand extends Command {
 	refresh() {
 		const selection = this.editor.model.document.selection;
 
-		const tableParent = getParentTable( selection.getFirstPosition() );
+		const tableParent = findAncestor( 'table', selection.getFirstPosition() );
 
 		this.isEnabled = !!tableParent;
 	}
@@ -72,8 +72,10 @@ export default class InsertColumnCommand extends Command {
 		const selection = editor.model.document.selection;
 		const tableUtils = editor.plugins.get( TableUtils );
 
-		const table = getParentTable( selection.getFirstPosition() );
-		const tableCell = selection.getFirstPosition().parent;
+		const firstPosition = selection.getFirstPosition();
+
+		const tableCell = findAncestor( 'tableCell', firstPosition );
+		const table = tableCell.parent.parent;
 
 		const { column } = tableUtils.getCellLocation( tableCell );
 		const insertAt = this.order === 'after' ? column + 1 : column;

+ 6 - 5
packages/ckeditor5-table/src/commands/insertrowcommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getParentTable } from './utils';
+import { findAncestor } from './utils';
 import TableUtils from '../tableutils';
 
 /**
@@ -54,7 +54,7 @@ export default class InsertRowCommand extends Command {
 	refresh() {
 		const selection = this.editor.model.document.selection;
 
-		const tableParent = getParentTable( selection.getFirstPosition() );
+		const tableParent = findAncestor( 'table', selection.getFirstPosition() );
 
 		this.isEnabled = !!tableParent;
 	}
@@ -71,10 +71,11 @@ export default class InsertRowCommand extends Command {
 		const selection = editor.model.document.selection;
 		const tableUtils = editor.plugins.get( TableUtils );
 
-		const tableCell = selection.getFirstPosition().parent;
-		const table = getParentTable( selection.getFirstPosition() );
+		const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() );
+		const tableRow = tableCell.parent;
+		const table = tableRow.parent;
 
-		const row = table.getChildIndex( tableCell.parent );
+		const row = table.getChildIndex( tableRow );
 		const insertAt = this.order === 'below' ? row + 1 : row;
 
 		tableUtils.insertRows( table, { rows: 1, at: insertAt } );

+ 1 - 1
packages/ckeditor5-table/src/commands/inserttablecommand.js

@@ -62,7 +62,7 @@ export default class InsertTableCommand extends Command {
 		model.change( writer => {
 			const table = tableUtils.createTable( insertPosition, rows, columns );
 
-			writer.setSelection( Position.createAt( table.getChild( 0 ).getChild( 0 ) ) );
+			writer.setSelection( Position.createAt( table.getChild( 0 ).getChild( 0 ).getChild( 0 ) ) );
 		} );
 	}
 }

+ 36 - 10
packages/ckeditor5-table/src/commands/mergecellcommand.js

@@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import TableWalker from '../tablewalker';
-import { updateNumericAttribute } from './utils';
+import { findAncestor, updateNumericAttribute } from './utils';
 import TableUtils from '../tableutils';
 
 /**
@@ -83,7 +83,7 @@ export default class MergeCellCommand extends Command {
 	execute() {
 		const model = this.editor.model;
 		const doc = model.document;
-		const tableCell = doc.selection.getFirstPosition().parent;
+		const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
 		const cellToMerge = this.value;
 		const direction = this.direction;
 
@@ -97,9 +97,7 @@ export default class MergeCellCommand extends Command {
 			// Cache the parent of cell to remove for later check.
 			const removedTableCellRow = cellToRemove.parent;
 
-			// Remove table cell and merge it contents with merged cell.
-			writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) );
-			writer.remove( cellToRemove );
+			mergeTableCells( cellToRemove, cellToExpand, writer );
 
 			const spanAttribute = this.isHorizontal ? 'colspan' : 'rowspan';
 			const cellSpan = parseInt( tableCell.getAttribute( spanAttribute ) || 1 );
@@ -125,9 +123,9 @@ export default class MergeCellCommand extends Command {
 	_getMergeableCell() {
 		const model = this.editor.model;
 		const doc = model.document;
-		const element = doc.selection.getFirstPosition().parent;
+		const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
 
-		if ( !element.is( 'tableCell' ) ) {
+		if ( !tableCell ) {
 			return;
 		}
 
@@ -135,8 +133,8 @@ export default class MergeCellCommand extends Command {
 
 		// First get the cell on proper direction.
 		const cellToMerge = this.isHorizontal ?
-			getHorizontalCell( element, this.direction, tableUtils ) :
-			getVerticalCell( element, this.direction );
+			getHorizontalCell( tableCell, this.direction, tableUtils ) :
+			getVerticalCell( tableCell, this.direction );
 
 		if ( !cellToMerge ) {
 			return;
@@ -144,7 +142,7 @@ export default class MergeCellCommand extends Command {
 
 		// If found check if the span perpendicular to merge direction is equal on both cells.
 		const spanAttribute = this.isHorizontal ? 'rowspan' : 'colspan';
-		const span = parseInt( element.getAttribute( spanAttribute ) || 1 );
+		const span = parseInt( tableCell.getAttribute( spanAttribute ) || 1 );
 
 		const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 );
 
@@ -254,3 +252,31 @@ function removeEmptyRow( removedTableCellRow, writer ) {
 
 	writer.remove( removedTableCellRow );
 }
+
+// Merges two table cells - will ensure that after merging cells with empty paragraph the result table cell will only have one paragraph.
+// If one of the merged table cell is empty the merged table cell will have contents of the non-empty table cell.
+// If both are empty the merged table cell will have only one empty paragraph.
+//
+// @param {module:engine/model/element~Element} cellToRemove
+// @param {module:engine/model/element~Element} cellToExpand
+// @param {module:engine/model/writer~Writer} writer
+function mergeTableCells( cellToRemove, cellToExpand, writer ) {
+	if ( !isEmpty( cellToRemove ) ) {
+		if ( isEmpty( cellToExpand ) ) {
+			writer.remove( Range.createIn( cellToExpand ) );
+		}
+
+		writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) );
+	}
+
+	// Remove merged table cell.
+	writer.remove( cellToRemove );
+}
+
+// Checks if passed table cell contains empty paragraph.
+//
+// @param {module:engine/model/element~Element} tableCell
+// @returns {Boolean}
+function isEmpty( tableCell ) {
+	return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'paragraph' ) && tableCell.getChild( 0 ).isEmpty;
+}

+ 4 - 4
packages/ckeditor5-table/src/commands/removecolumncommand.js

@@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import TableWalker from '../tablewalker';
 import TableUtils from '../tableutils';
-import { updateNumericAttribute } from './utils';
+import { findAncestor, updateNumericAttribute } from './utils';
 
 /**
  * The remove column command.
@@ -33,9 +33,9 @@ export default class RemoveColumnCommand extends Command {
 		const selection = editor.model.document.selection;
 		const tableUtils = editor.plugins.get( TableUtils );
 
-		const selectedElement = selection.getFirstPosition().parent;
+		const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() );
 
-		this.isEnabled = selectedElement.is( 'tableCell' ) && tableUtils.getColumns( selectedElement.parent.parent ) > 1;
+		this.isEnabled = !!tableCell && tableUtils.getColumns( tableCell.parent.parent ) > 1;
 	}
 
 	/**
@@ -47,7 +47,7 @@ export default class RemoveColumnCommand extends Command {
 
 		const firstPosition = selection.getFirstPosition();
 
-		const tableCell = firstPosition.parent;
+		const tableCell = findAncestor( 'tableCell', firstPosition );
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 

+ 4 - 4
packages/ckeditor5-table/src/commands/removerowcommand.js

@@ -12,7 +12,7 @@ import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 
 import TableWalker from '../tablewalker';
-import { updateNumericAttribute } from './utils';
+import { findAncestor, updateNumericAttribute } from './utils';
 
 /**
  * The remove row command.
@@ -33,9 +33,9 @@ export default class RemoveRowCommand extends Command {
 		const model = this.editor.model;
 		const doc = model.document;
 
-		const element = doc.selection.getFirstPosition().parent;
+		const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
 
-		this.isEnabled = element.is( 'tableCell' ) && element.parent.parent.childCount > 1;
+		this.isEnabled = !!tableCell && tableCell.parent.parent.childCount > 1;
 	}
 
 	/**
@@ -46,7 +46,7 @@ export default class RemoveRowCommand extends Command {
 		const selection = model.document.selection;
 
 		const firstPosition = selection.getFirstPosition();
-		const tableCell = firstPosition.parent;
+		const tableCell = findAncestor( 'tableCell', firstPosition );
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 

+ 5 - 5
packages/ckeditor5-table/src/commands/setheadercolumncommand.js

@@ -9,7 +9,7 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
-import { getParentTable, updateNumericAttribute } from './utils';
+import { findAncestor, updateNumericAttribute } from './utils';
 
 /**
  * The header column command.
@@ -36,9 +36,9 @@ export default class SetHeaderColumnCommand extends Command {
 		const selection = doc.selection;
 
 		const position = selection.getFirstPosition();
-		const tableParent = getParentTable( position );
+		const tableCell = findAncestor( 'tableCell', position );
 
-		const isInTable = !!tableParent;
+		const isInTable = !!tableCell;
 
 		this.isEnabled = isInTable;
 
@@ -50,7 +50,7 @@ export default class SetHeaderColumnCommand extends Command {
 		 * @readonly
 		 * @member {Boolean} #value
 		 */
-		this.value = isInTable && this._isInHeading( position.parent, tableParent );
+		this.value = isInTable && this._isInHeading( tableCell, tableCell.parent.parent );
 	}
 
 	/**
@@ -69,7 +69,7 @@ export default class SetHeaderColumnCommand extends Command {
 		const tableUtils = this.editor.plugins.get( 'TableUtils' );
 
 		const position = selection.getFirstPosition();
-		const tableCell = position.parent;
+		const tableCell = findAncestor( 'tableCell', position.parent );
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 

+ 7 - 7
packages/ckeditor5-table/src/commands/setheaderrowcommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 
-import { getParentTable, updateNumericAttribute } from './utils';
+import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './utils';
 import TableWalker from '../tablewalker';
 
 /**
@@ -37,9 +37,8 @@ export default class SetHeaderRowCommand extends Command {
 		const selection = doc.selection;
 
 		const position = selection.getFirstPosition();
-		const tableParent = getParentTable( position );
-
-		const isInTable = !!tableParent;
+		const tableCell = findAncestor( 'tableCell', position );
+		const isInTable = !!tableCell;
 
 		this.isEnabled = isInTable;
 
@@ -51,7 +50,7 @@ export default class SetHeaderRowCommand extends Command {
 		 * @readonly
 		 * @member {Boolean} #value
 		 */
-		this.value = isInTable && this._isInHeading( position.parent, tableParent );
+		this.value = isInTable && this._isInHeading( tableCell, tableCell.parent.parent );
 	}
 
 	/**
@@ -69,7 +68,7 @@ export default class SetHeaderRowCommand extends Command {
 		const selection = doc.selection;
 
 		const position = selection.getFirstPosition();
-		const tableCell = position.parent;
+		const tableCell = findAncestor( 'tableCell', position );
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 
@@ -170,8 +169,9 @@ function splitHorizontally( tableCell, headingRows, writer ) {
 
 		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
 			const tableRow = table.getChild( row );
+			const tableCellPosition = Position.createFromParentAndOffset( tableRow, cellIndex );
 
-			writer.insertElement( 'tableCell', attributes, Position.createFromParentAndOffset( tableRow, cellIndex ) );
+			createEmptyTableCell( writer, tableCellPosition, attributes );
 		}
 	}
 

+ 4 - 3
packages/ckeditor5-table/src/commands/splitcellcommand.js

@@ -9,6 +9,7 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import TableUtils from '../tableutils';
+import { findAncestor } from './utils';
 
 /**
  * The split cell command.
@@ -49,9 +50,9 @@ export default class SplitCellCommand extends Command {
 		const model = this.editor.model;
 		const doc = model.document;
 
-		const element = doc.selection.getFirstPosition().parent;
+		const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
 
-		this.isEnabled = element.is( 'tableCell' );
+		this.isEnabled = !!tableCell;
 	}
 
 	/**
@@ -63,7 +64,7 @@ export default class SplitCellCommand extends Command {
 		const selection = document.selection;
 
 		const firstPosition = selection.getFirstPosition();
-		const tableCell = firstPosition.parent;
+		const tableCell = findAncestor( 'tableCell', firstPosition );
 
 		const isHorizontally = this.direction === 'horizontally';
 

+ 18 - 4
packages/ckeditor5-table/src/commands/utils.js

@@ -8,16 +8,17 @@
  */
 
 /**
- * Returns the parent table.
+ * Returns the parent element of given name. Returns undefined if position is not inside desired parent.
  *
- * @param {module:engine/model/position~Position} position
+ * @param {String} parentName Name of parent element to find.
+ * @param {module:engine/model/position~Position|module:engine/model/position~Position} position Position to start searching.
  * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
  */
-export function getParentTable( position ) {
+export function findAncestor( parentName, position ) {
 	let parent = position.parent;
 
 	while ( parent ) {
-		if ( parent.name === 'table' ) {
+		if ( parent.name === parentName ) {
 			return parent;
 		}
 
@@ -41,3 +42,16 @@ export function updateNumericAttribute( key, value, item, writer, defaultValue =
 		writer.removeAttribute( key, item );
 	}
 }
+
+/**
+ * Common method to create empty table cell - it will create proper model structure as table cell must have at least one block inside.
+ *
+ * @param {module:engine/model/writer~Writer} writer Model writer.
+ * @param {module:engine/model/position~Position} insertPosition Position at which table cell should be inserted.
+ * @param {Object} attributes Element's attributes.
+ */
+export function createEmptyTableCell( writer, insertPosition, attributes = {} ) {
+	const tableCell = writer.createElement( 'tableCell', attributes );
+	writer.insertElement( 'paragraph', tableCell );
+	writer.insert( tableCell, insertPosition );
+}

+ 30 - 6
packages/ckeditor5-table/src/converters/downcast.js

@@ -74,7 +74,7 @@ export function downcastInsertTable( options = {} ) {
 
 		conversionApi.mapper.bindElements( table, asWidget ? tableWidget : figureElement );
 		conversionApi.writer.insert( viewPosition, asWidget ? tableWidget : figureElement );
-	}, { priority: 'normal' } );
+	} );
 }
 
 /**
@@ -117,7 +117,7 @@ export function downcastInsertRow( options = {} ) {
 
 			createViewTableCellElement( tableWalkerValue, tableAttributes, insertPosition, conversionApi, options );
 		}
-	}, { priority: 'normal' } );
+	} );
 }
 
 /**
@@ -159,7 +159,7 @@ export function downcastInsertCell( options = {} ) {
 				return;
 			}
 		}
-	}, { priority: 'normal' } );
+	} );
 }
 
 /**
@@ -236,7 +236,7 @@ export function downcastTableHeadingRowsChange( options = {} ) {
 		function isBetween( index, lower, upper ) {
 			return index > lower && index < upper;
 		}
-	}, { priority: 'normal' } );
+	} );
 }
 
 /**
@@ -274,7 +274,7 @@ export function downcastTableHeadingColumnsChange( options = {} ) {
 
 			renameViewTableCellIfRequired( tableWalkerValue, tableAttributes, conversionApi, asWidget );
 		}
-	}, { priority: 'normal' } );
+	} );
 }
 
 /**
@@ -375,8 +375,32 @@ function createViewTableCellElement( tableWalkerValue, tableAttributes, insertPo
 
 	const tableCell = tableWalkerValue.cell;
 
-	conversionApi.mapper.bindElements( tableCell, cellElement );
+	const isSingleParagraph = tableCell.childCount === 1 && tableCell.getChild( 0 ).name === 'paragraph';
+
 	conversionApi.writer.insert( insertPosition, cellElement );
+
+	if ( isSingleParagraph ) {
+		const innerParagraph = tableCell.getChild( 0 );
+		const paragraphInsertPosition = ViewPosition.createAt( cellElement, 'end' );
+
+		conversionApi.consumable.consume( innerParagraph, 'insert' );
+
+		if ( options.asWidget ) {
+			const containerName = [ ...innerParagraph.getAttributeKeys() ].length ? 'p' : 'span';
+
+			const fakeParagraph = conversionApi.writer.createContainerElement( containerName );
+
+			conversionApi.mapper.bindElements( innerParagraph, fakeParagraph );
+			conversionApi.writer.insert( paragraphInsertPosition, fakeParagraph );
+
+			conversionApi.mapper.bindElements( tableCell, cellElement );
+		} else {
+			conversionApi.mapper.bindElements( tableCell, cellElement );
+			conversionApi.mapper.bindElements( innerParagraph, cellElement );
+		}
+	} else {
+		conversionApi.mapper.bindElements( tableCell, cellElement );
+	}
 }
 
 // Creates or returns an existing `<tr>` element from the view.

+ 4 - 4
packages/ckeditor5-table/src/converters/table-post-fixer.js

@@ -8,7 +8,7 @@
  */
 
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
-import { getParentTable, updateNumericAttribute } from './../commands/utils';
+import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './../commands/utils';
 import TableWalker from './../tablewalker';
 
 /**
@@ -240,12 +240,12 @@ function tablePostFixer( writer, model ) {
 
 		// Fix table on adding/removing table cells and rows.
 		if ( entry.name == 'tableRow' || entry.name == 'tableCell' ) {
-			table = getParentTable( entry.position );
+			table = findAncestor( 'table', entry.position );
 		}
 
 		// Fix table on any table's attribute change - including attributes of table cells.
 		if ( isTableAttributeEntry( entry ) ) {
-			table = getParentTable( entry.range.start );
+			table = findAncestor( 'table', entry.range.start );
 		}
 
 		if ( table && !analyzedTables.has( table ) ) {
@@ -304,7 +304,7 @@ function fixTableRowsSizes( table, writer ) {
 
 			if ( columnsToInsert ) {
 				for ( let i = 0; i < columnsToInsert; i++ ) {
-					writer.insertElement( 'tableCell', Position.createAt( table.getChild( rowIndex ), 'end' ) );
+					createEmptyTableCell( writer, Position.createAt( table.getChild( rowIndex ), 'end' ) );
 				}
 
 				wasFixed = true;

+ 175 - 0
packages/ckeditor5-table/src/converters/tablecell-post-fixer.js

@@ -0,0 +1,175 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/converters/tablecell-post-fixer
+ */
+
+/**
+ * Injects a table cell post-fixer into the editing controller.
+ *
+ * The role of the table cell post-fixer is to ensure that the table cell contents in the editing view are properly converted.
+ *
+ * This post-fixer will ensure that after model changes in the editing view:
+ * * single paragraphs are rendered as `<span>
+ * * single paragraphs with one or more attributes are rendered as `<p>`
+ * * single paragraphs in table cell with other block elements are rendered as `<p>`
+ * * paragraphs in table cells with other block elements (including other paragraphs) are rendered as `<p>`.
+ *
+ * In the model each table cell has always at least one block element inside. If no other block was defined (empty table cell) the table
+ * feature will insert empty `<paragraph>`. Similarly text nodes will be wrapped in paragraphs. Rendering in the data pipeline differs
+ * from rendering in the editing pipeline - text nodes in single `<paragraph>` are rendered in the data pipeline as direct children
+ * of the `<td>` or `<th>` elements. In other cases `<paragraph>` elements are rendered as `<p>` blocks.
+ *
+ * To ensure proper mappings between model and view elements and positions in the editing pipeline the table feature will always render
+ * an element in the view: `<span>` for single or empty `<paragraph>` and `<p>` otherwise.
+ *
+ * Example:
+ *
+ *		<table>
+ *			<tableRow>
+ *				<tableCell><paragraph></paragraph></tableCell>
+ *				<tableCell><paragraph>foo</paragraph></tableCell>
+ *				<tableCell><paragraph baz="bar">foo</paragraph></tableCell>
+ *				<tableCell><heading2>bar</heading2><paragraph>baz</paragraph></tableCell>
+ *			</tableRow>
+ *		</table>
+ *
+ * The editor will render in the data pipeline:
+ *
+ *		<figure>
+ *			<table>
+ *				<tbody>
+ *					<tr>
+ *						<td></td>
+ *						<td>foo</td>
+ *						<td><p baz="bar">foo</p></td>
+ *						<td><h3>bar</h3><p>baz</p></td>
+ *					</tr>
+ *				</tbody>
+ *			</table>
+ *		</figure>
+ *
+ * and in the editing view (without widget markup):
+ *
+ *		<figure>
+ *			<table>
+ *				<tbody>
+ *					<tr>
+ *						<td><span></span></td>
+ *						<td><span>foo</span></td>
+ *						<td><p baz="bar">foo</p></td>
+ *						<td><h3>bar</h3><p>baz</p></td>
+ *					</tr>
+ *				</tbody>
+ *			</table>
+ *		</figure>
+ *
+ * @param {module:engine/model/model~Model} model
+ * @param {module:engine/controller/editingcontroller~EditingController} editing
+ */
+export default function injectTableCellPostFixer( model, editing ) {
+	editing.view.document.registerPostFixer( writer => tableCellPostFixer( writer, model, editing.mapper ) );
+}
+
+// The table cell post-fixer.
+//
+// @param {module:engine/view/writer~Writer} writer
+// @param {module:engine/model/model~Model} model
+// @param {module:engine/conversion/mapper~Mapper} mapper
+function tableCellPostFixer( writer, model, mapper ) {
+	const changes = model.document.differ.getChanges();
+	let wasFixed = false;
+
+	// While this is view post fixer only nodes that changed are worth investigating.
+	for ( const entry of changes ) {
+		// Attribute change - check if it is single paragraph inside table cell that has attributes changed.
+		if ( entry.type == 'attribute' && entry.range.start.parent.name == 'tableCell' ) {
+			const tableCell = entry.range.start.parent;
+
+			if ( tableCell.childCount === 1 ) {
+				const singleChild = tableCell.getChild( 0 );
+				const renameTo = Array.from( singleChild.getAttributes() ).length ? 'p' : 'span';
+
+				wasFixed = renameParagraphIfDifferent( singleChild, renameTo, writer, model, mapper ) || wasFixed;
+			}
+		} else {
+			// Check all nodes inside table cell on insert/remove operations (also other blocks).
+			const parent = entry.position && entry.position.parent;
+
+			if ( parent && parent.is( 'tableCell' ) ) {
+				const renameTo = parent.childCount > 1 ? 'p' : 'span';
+
+				for ( const child of parent.getChildren() ) {
+					wasFixed = renameParagraphIfDifferent( child, renameTo, writer, model, mapper ) || wasFixed;
+				}
+			}
+		}
+	}
+
+	return wasFixed;
+}
+
+// Renames associated view element to a desired one. It will only rename if:
+// - model element is a paragraph
+// - view element is converted (mapped)
+// - view element has different name then requested.
+//
+// @param {module:engine/model/element~Element} modelElement
+// @param {String} desiredElementName
+// @param {module:engine/view/writer~Writer} writer
+// @param {module:engine/model/model~Model} model
+// @param {module:engine/conversion/mapper~Mapper} mapper
+function renameParagraphIfDifferent( modelElement, desiredElementName, writer, model, mapper ) {
+	// Only rename paragraph elements.
+	if ( !modelElement.is( 'paragraph' ) ) {
+		return false;
+	}
+
+	const viewElement = mapper.toViewElement( modelElement );
+
+	// Only rename converted elements which aren't desired ones.
+	if ( !viewElement || viewElement.name === desiredElementName ) {
+		return false;
+	}
+
+	// After renaming element in the view by a post-fixer the selection would have references to the previous element.
+	const selection = model.document.selection;
+	const shouldFixSelection = checkSelectionForRenamedElement( selection, modelElement );
+
+	// Unbind current view element as it should be cleared from mapper.
+	mapper.unbindViewElement( viewElement );
+	const renamedViewElement = writer.rename( viewElement, desiredElementName );
+	// Bind paragraph inside table cell to the renamed view element.
+	mapper.bindElements( modelElement, renamedViewElement );
+
+	if ( shouldFixSelection ) {
+		// Re-create view selection based on model selection.
+		updateRangesInViewSelection( selection, mapper, writer );
+	}
+
+	return true;
+}
+
+// Checks if model selection contains renamed element.
+//
+// @param {module:engine/model/selection~Selection} selection
+// @param {module:engine/model/element~Element} modelElement
+// @returns {boolean}
+function checkSelectionForRenamedElement( selection, modelElement ) {
+	return !![ ...selection.getSelectedBlocks() ].find( block => block === modelElement );
+}
+
+// Re-create view selection from model selection.
+//
+// @param {module:engine/model/selection~Selection} selection
+// @param {module:engine/view/writer~Writer} writer
+// @param {module:engine/conversion/mapper~Mapper} mapper
+function updateRangesInViewSelection( selection, mapper, writer ) {
+	const fixedRanges = Array.from( selection.getRanges() )
+		.map( range => mapper.toViewRange( range ) );
+
+	writer.setSelection( fixedRanges, { backward: selection.isBackward } );
+}

+ 59 - 3
packages/ckeditor5-table/src/converters/upcasttable.js

@@ -9,6 +9,7 @@
 
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
+import { createEmptyTableCell } from '../commands/utils';
 
 /**
  * View table element to model table element conversion helper.
@@ -44,6 +45,12 @@ export default function upcastTable() {
 
 			// Insert element on allowed position.
 			const splitResult = conversionApi.splitToAllowedParent( table, data.modelCursor );
+
+			// When there is no split result it means that we can't insert element to model tree, so let's skip it.
+			if ( !splitResult ) {
+				return;
+			}
+
 			conversionApi.writer.insert( table, splitResult.position );
 			conversionApi.consumable.consume( viewTable, { name: true } );
 
@@ -53,9 +60,9 @@ export default function upcastTable() {
 			} else {
 				// Create one row and one table cell for empty table.
 				const row = conversionApi.writer.createElement( 'tableRow' );
-
 				conversionApi.writer.insert( row, ModelPosition.createAt( table, 'end' ) );
-				conversionApi.writer.insertElement( 'tableCell', ModelPosition.createAt( row, 'end' ) );
+
+				createEmptyTableCell( conversionApi.writer, ModelPosition.createAt( row, 'end' ) );
 			}
 
 			// Set conversion result range.
@@ -81,7 +88,56 @@ export default function upcastTable() {
 			} else {
 				data.modelCursor = data.modelRange.end;
 			}
-		}, { priority: 'normal' } );
+		} );
+	};
+}
+
+export function upcastTableCell( elementName ) {
+	return dispatcher => {
+		dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
+			const viewTableCell = data.viewItem;
+
+			// When element was already consumed then skip it.
+			if ( !conversionApi.consumable.test( viewTableCell, { name: true } ) ) {
+				return;
+			}
+
+			const tableCell = conversionApi.writer.createElement( 'tableCell' );
+
+			// Insert element on allowed position.
+			const splitResult = conversionApi.splitToAllowedParent( tableCell, data.modelCursor );
+
+			// When there is no split result it means that we can't insert element to model tree, so let's skip it.
+			if ( !splitResult ) {
+				return;
+			}
+
+			conversionApi.writer.insert( tableCell, splitResult.position );
+			conversionApi.consumable.consume( viewTableCell, { name: true } );
+
+			for ( const child of viewTableCell.getChildren() ) {
+				const { modelCursor } = conversionApi.convertItem( child, ModelPosition.createAt( tableCell, 'end' ) );
+
+				// Ensure empty paragraph in table cell.
+				if ( modelCursor.parent.name == 'tableCell' && !modelCursor.parent.childCount ) {
+					conversionApi.writer.insertElement( 'paragraph', modelCursor );
+				}
+			}
+
+			// Set conversion result range.
+			data.modelRange = new ModelRange(
+				// Range should start before inserted element
+				ModelPosition.createBefore( tableCell ),
+				// 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( tableCell )
+			);
+
+			// Continue after inserted element.
+			data.modelCursor = data.modelRange.end;
+		} );
 	};
 }
 

+ 77 - 71
packages/ckeditor5-table/src/tableediting.js

@@ -10,9 +10,8 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
-import upcastTable from './converters/upcasttable';
+import upcastTable, { upcastTableCell } from './converters/upcasttable';
 import {
 	downcastInsertCell,
 	downcastInsertRow,
@@ -31,9 +30,11 @@ import RemoveRowCommand from './commands/removerowcommand';
 import RemoveColumnCommand from './commands/removecolumncommand';
 import SetHeaderRowCommand from './commands/setheaderrowcommand';
 import SetHeaderColumnCommand from './commands/setheadercolumncommand';
-import { getParentTable } from './commands/utils';
+import { findAncestor } from './commands/utils';
 import TableUtils from '../src/tableutils';
+
 import injectTablePostFixer from './converters/table-post-fixer';
+import injectTableCellPostFixer from './converters/tablecell-post-fixer';
 
 import '../theme/tableediting.css';
 
@@ -66,11 +67,27 @@ export default class TableEditing extends Plugin {
 
 		schema.register( 'tableCell', {
 			allowIn: 'tableRow',
-			allowContentOf: '$block',
 			allowAttributes: [ 'colspan', 'rowspan' ],
 			isLimit: true
 		} );
 
+		// Allow all $block content inside table cell.
+		schema.extend( '$block', { allowIn: 'tableCell' } );
+
+		// Disallow table in table.
+		schema.addChildCheck( ( context, childDefinition ) => {
+			if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) {
+				return false;
+			}
+		} );
+
+		// Disallow image in table cell.
+		schema.addChildCheck( ( context, childDefinition ) => {
+			if ( childDefinition.name == 'image' && Array.from( context.getNames() ).includes( 'table' ) ) {
+				return false;
+			}
+		} );
+
 		// Table conversion.
 		conversion.for( 'upcast' ).add( upcastTable() );
 
@@ -85,8 +102,8 @@ export default class TableEditing extends Plugin {
 		conversion.for( 'downcast' ).add( downcastRemoveRow() );
 
 		// Table cell conversion.
-		conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-		conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
+		conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
+		conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );
 
 		conversion.for( 'editingDowncast' ).add( downcastInsertCell( { asWidget: true } ) );
 		conversion.for( 'dataDowncast' ).add( downcastInsertCell() );
@@ -101,6 +118,8 @@ export default class TableEditing extends Plugin {
 		conversion.for( 'editingDowncast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
 		conversion.for( 'dataDowncast' ).add( downcastTableHeadingRowsChange() );
 
+		injectTableCellPostFixer( editor.model, editor.editing );
+
 		// Define all the commands.
 		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
 		editor.commands.add( 'insertTableRowAbove', new InsertRowCommand( editor, { order: 'above' } ) );
@@ -125,8 +144,9 @@ export default class TableEditing extends Plugin {
 		injectTablePostFixer( model );
 
 		// Handle tab key navigation.
-		this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabOnSelectedTable( ...args ) );
-		this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabInsideTable( ...args ) );
+		this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );
+		this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
+		this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
 	}
 
 	/**
@@ -144,27 +164,18 @@ export default class TableEditing extends Plugin {
 	 * @param {module:utils/eventinfo~EventInfo} eventInfo
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 */
-	_handleTabOnSelectedTable( eventInfo, domEventData ) {
-		const tabPressed = domEventData.keyCode == keyCodes.tab;
-
-		// Act only on TAB & SHIFT-TAB - Do not override native CTRL+TAB handler.
-		if ( !tabPressed || domEventData.ctrlKey ) {
-			return;
-		}
-
+	_handleTabOnSelectedTable( domEventData, cancel ) {
 		const editor = this.editor;
 		const selection = editor.model.document.selection;
 
 		if ( !selection.isCollapsed && selection.rangeCount === 1 && selection.getFirstRange().isFlat ) {
 			const selectedElement = selection.getSelectedElement();
 
-			if ( !selectedElement || selectedElement.name != 'table' ) {
+			if ( !selectedElement || !selectedElement.is( 'table' ) ) {
 				return;
 			}
 
-			eventInfo.stop();
-			domEventData.preventDefault();
-			domEventData.stopPropagation();
+			cancel();
 
 			editor.model.change( writer => {
 				writer.setSelection( Range.createIn( selectedElement.getChild( 0 ).getChild( 0 ) ) );
@@ -173,75 +184,70 @@ export default class TableEditing extends Plugin {
 	}
 
 	/**
-	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed inside table
-	 * cell.
+	 * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
+	 * inside table cell.
 	 *
 	 * @private
-	 * @param {module:utils/eventinfo~EventInfo} eventInfo
-	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 * @param {Boolean} isForward Whether this handler will move selection to the next cell or previous.
 	 */
-	_handleTabInsideTable( eventInfo, domEventData ) {
-		const tabPressed = domEventData.keyCode == keyCodes.tab;
+	_getTabHandler( isForward ) {
+		const editor = this.editor;
 
-		// Act only on TAB & SHIFT-TAB - Do not override native CTRL+TAB handler.
-		if ( !tabPressed || domEventData.ctrlKey ) {
-			return;
-		}
+		return ( domEventData, cancel ) => {
+			const selection = editor.model.document.selection;
 
-		const editor = this.editor;
-		const selection = editor.model.document.selection;
+			const firstPosition = selection.getFirstPosition();
 
-		const table = getParentTable( selection.getFirstPosition() );
+			const tableCell = findAncestor( 'tableCell', firstPosition );
 
-		if ( !table ) {
-			return;
-		}
+			if ( !tableCell ) {
+				return;
+			}
 
-		domEventData.preventDefault();
-		domEventData.stopPropagation();
+			cancel();
 
-		const tableCell = selection.focus.parent;
-		const tableRow = tableCell.parent;
+			const tableRow = tableCell.parent;
+			const table = tableRow.parent;
 
-		const currentRowIndex = table.getChildIndex( tableRow );
-		const currentCellIndex = tableRow.getChildIndex( tableCell );
+			const currentRowIndex = table.getChildIndex( tableRow );
+			const currentCellIndex = tableRow.getChildIndex( tableCell );
 
-		const isForward = !domEventData.shiftKey;
-		const isFirstCellInRow = currentCellIndex === 0;
+			const isFirstCellInRow = currentCellIndex === 0;
 
-		if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
-			// It's the first cell of a table - don't do anything (stay in current position).
-			return;
-		}
+			if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
+				// It's the first cell of a table - don't do anything (stay in current position).
+				return;
+			}
 
-		const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
-		const isLastRow = currentRowIndex === table.childCount - 1;
+			const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
+			const isLastRow = currentRowIndex === table.childCount - 1;
 
-		if ( isForward && isLastRow && isLastCellInRow ) {
-			editor.plugins.get( TableUtils ).insertRows( table, { at: table.childCount } );
-		}
+			if ( isForward && isLastRow && isLastCellInRow ) {
+				editor.plugins.get( TableUtils ).insertRows( table, { at: table.childCount } );
+			}
 
-		let cellToFocus;
+			let cellToFocus;
 
-		// Move to first cell in next row.
-		if ( isForward && isLastCellInRow ) {
-			const nextRow = table.getChild( currentRowIndex + 1 );
+			// Move to first cell in next row.
+			if ( isForward && isLastCellInRow ) {
+				const nextRow = table.getChild( currentRowIndex + 1 );
 
-			cellToFocus = nextRow.getChild( 0 );
-		}
-		// Move to last cell in a previous row.
-		else if ( !isForward && isFirstCellInRow ) {
-			const previousRow = table.getChild( currentRowIndex - 1 );
+				cellToFocus = nextRow.getChild( 0 );
+			}
+			// Move to last cell in a previous row.
+			else if ( !isForward && isFirstCellInRow ) {
+				const previousRow = table.getChild( currentRowIndex - 1 );
 
-			cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
-		}
-		// Move to next/previous cell.
-		else {
-			cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
-		}
+				cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
+			}
+			// Move to next/previous cell.
+			else {
+				cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
+			}
 
-		editor.model.change( writer => {
-			writer.setSelection( Range.createIn( cellToFocus ) );
-		} );
+			editor.model.change( writer => {
+				writer.setSelection( Range.createIn( cellToFocus ) );
+			} );
+		};
 	}
 }

+ 8 - 6
packages/ckeditor5-table/src/tableutils.js

@@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 
 import TableWalker from './tablewalker';
-import { getParentTable, updateNumericAttribute } from './commands/utils';
+import { createEmptyTableCell, updateNumericAttribute } from './commands/utils';
 
 /**
  * The table utilities plugin.
@@ -294,7 +294,8 @@ export default class TableUtils extends Plugin {
 	 */
 	splitCellVertically( tableCell, numberOfCells = 2 ) {
 		const model = this.editor.model;
-		const table = getParentTable( tableCell );
+		const tableRow = tableCell.parent;
+		const table = tableRow.parent;
 
 		const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
 		const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
@@ -430,8 +431,9 @@ export default class TableUtils extends Plugin {
 	splitCellHorizontally( tableCell, numberOfCells = 2 ) {
 		const model = this.editor.model;
 
-		const table = getParentTable( tableCell );
-		const splitCellRow = table.getChildIndex( tableCell.parent );
+		const tableRow = tableCell.parent;
+		const table = tableRow.parent;
+		const splitCellRow = table.getChildIndex( tableRow );
 
 		const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
 		const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
@@ -480,7 +482,7 @@ export default class TableUtils extends Plugin {
 					if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) {
 						const position = Position.createFromParentAndOffset( table.getChild( row ), cellIndex );
 
-						writer.insertElement( 'tableCell', newCellsAttributes, position );
+						createCells( 1, writer, position, newCellsAttributes );
 					}
 				}
 			}
@@ -569,7 +571,7 @@ function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attr
 // @param {module:engine/model/position~Position} insertPosition
 function createCells( cells, writer, insertPosition, attributes = {} ) {
 	for ( let i = 0; i < cells; i++ ) {
-		writer.insertElement( 'tableCell', attributes, insertPosition );
+		createEmptyTableCell( writer, insertPosition, attributes );
 	}
 }
 

+ 2 - 2
packages/ckeditor5-table/src/ui/utils.js

@@ -8,7 +8,7 @@
  */
 
 import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
-import { getParentTable } from '../commands/utils';
+import { findAncestor } from '../commands/utils';
 
 /**
  * A helper utility that positions the
@@ -36,7 +36,7 @@ export function getBalloonPositionData( editor ) {
 	const defaultPositions = BalloonPanelView.defaultPositions;
 	const viewSelection = editingView.document.selection;
 
-	const parentTable = getParentTable( viewSelection.getFirstPosition() );
+	const parentTable = findAncestor( 'table', viewSelection.getFirstPosition() );
 
 	return {
 		target: editingView.domConverter.viewToDom( parentTable ),

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

@@ -7,8 +7,8 @@
  * @module table/utils
  */
 
-import { toWidget, isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
-import { getParentTable } from './commands/utils';
+import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
+import { findAncestor } from './commands/utils';
 
 const tableSymbol = Symbol( 'isTable' );
 
@@ -57,7 +57,7 @@ export function isTableWidgetSelected( selection ) {
  * @returns {Boolean}
  */
 export function isTableContentSelected( selection ) {
-	const parentTable = getParentTable( selection.getFirstPosition() );
+	const parentTable = findAncestor( 'table', selection.getFirstPosition() );
 
 	return !!( parentTable && isTableWidget( parentTable.parent ) );
 }

+ 128 - 12
packages/ckeditor5-table/tests/_utils/utils.js

@@ -3,6 +3,18 @@
  * For licensing, see LICENSE.md.
  */
 
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import {
+	downcastInsertCell,
+	downcastInsertRow,
+	downcastInsertTable,
+	downcastRemoveRow,
+	downcastTableHeadingColumnsChange,
+	downcastTableHeadingRowsChange
+} from '../../src/converters/downcast';
+import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable';
+
 /**
  * Returns a model representation of a table shorthand notation:
  *
@@ -31,7 +43,13 @@
  * @returns {String}
  */
 export function modelTable( tableData, attributes ) {
-	const tableRows = makeRows( tableData, 'tableCell', 'tableRow', 'tableCell' );
+	const tableRows = makeRows( tableData, {
+		cellElement: 'tableCell',
+		rowElement: 'tableRow',
+		headingElement: 'tableCell',
+		wrappingElement: 'paragraph',
+		enforceWrapping: true
+	} );
 
 	return `<table${ formatAttributes( attributes ) }>${ tableRows }</table>`;
 }
@@ -67,15 +85,35 @@ export function modelTable( tableData, attributes ) {
  */
 export function viewTable( tableData, attributes = {} ) {
 	const headingRows = attributes.headingRows || 0;
+	const asWidget = !!attributes.asWidget;
+
+	const thead = headingRows > 0 ? `<thead>${ makeRows( tableData.slice( 0, headingRows ), {
+		cellElement: 'th',
+		rowElement: 'tr',
+		headingElement: 'th',
+		wrappingElement: asWidget ? 'span' : 'p',
+		enforceWrapping: asWidget,
+		asWidget
+	} ) }</thead>` : '';
+
+	const tbody = tableData.length > headingRows ?
+		`<tbody>${ makeRows( tableData.slice( headingRows ), {
+			cellElement: 'td',
+			rowElement: 'tr',
+			headingElement: 'th',
+			wrappingElement: asWidget ? 'span' : 'p',
+			enforceWrapping: asWidget,
+			asWidget
+		} ) }</tbody>` : '';
 
-	const thead = headingRows > 0 ? `<thead>${ makeRows( tableData.slice( 0, headingRows ), 'th', 'tr' ) }</thead>` : '';
-	const tbody = tableData.length > headingRows ? `<tbody>${ makeRows( tableData.slice( headingRows ), 'td', 'tr' ) }</tbody>` : '';
+	const figureAttributes = asWidget ? 'class="ck-widget ck-widget_selectable table" contenteditable="false"' : 'class="table"';
+	const widgetHandler = '<div class="ck ck-widget__selection-handler"></div>';
 
-	return `<figure class="table"><table>${ thead }${ tbody }</table></figure>`;
+	return `<figure ${ figureAttributes }>${ asWidget ? widgetHandler : '' }<table>${ thead }${ tbody }</table></figure>`;
 }
 
 /**
- * Formats model or view table - useful for chai assertions debuging.
+ * Formats model or view table - useful for chai assertions debugging.
  *
  * @param {String} tableString
  * @returns {String}
@@ -118,6 +156,72 @@ export function formattedViewTable( tableData, attributes ) {
 	return formatTable( viewTable( tableData, attributes ) );
 }
 
+export function defaultSchema( schema, registerParagraph = true ) {
+	schema.register( 'table', {
+		allowWhere: '$block',
+		allowAttributes: [ 'headingRows', 'headingColumns' ],
+		isLimit: true,
+		isObject: true
+	} );
+
+	schema.register( 'tableRow', {
+		allowIn: 'table',
+		isLimit: true
+	} );
+
+	schema.register( 'tableCell', {
+		allowIn: 'tableRow',
+		allowAttributes: [ 'colspan', 'rowspan' ],
+		isLimit: true
+	} );
+
+	// Allow all $block content inside table cell.
+	schema.extend( '$block', { allowIn: 'tableCell' } );
+
+	// Disallow table in table.
+	schema.addChildCheck( ( context, childDefinition ) => {
+		if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) {
+			return false;
+		}
+	} );
+
+	// Disallow image in table.
+	schema.addChildCheck( ( context, childDefinition ) => {
+		if ( childDefinition.name == 'image' && Array.from( context.getNames() ).includes( 'table' ) ) {
+			return false;
+		}
+	} );
+
+	if ( registerParagraph ) {
+		schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+	}
+}
+
+export function defaultConversion( conversion, asWidget = false ) {
+	conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+	// Table conversion.
+	conversion.for( 'upcast' ).add( upcastTable() );
+	conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget } ) );
+
+	// Table row conversion.
+	conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
+	conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget } ) );
+	conversion.for( 'downcast' ).add( downcastRemoveRow( { asWidget } ) );
+
+	// Table cell conversion.
+	conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
+	conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );
+	conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget } ) );
+
+	// Table attributes conversion.
+	conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
+	conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+
+	conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget } ) );
+	conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget } ) );
+}
+
 // Formats table cell attributes
 //
 // @param {Object} attributes Attributes of a cell.
@@ -131,23 +235,24 @@ function formatAttributes( attributes ) {
 			attributesString = ' ' + entries.map( entry => `${ entry[ 0 ] }="${ entry[ 1 ] }"` ).join( ' ' );
 		}
 	}
+
 	return attributesString;
 }
 
 // Formats passed table data to a set of table rows.
-function makeRows( tableData, cellElement, rowElement, headingElement = 'th' ) {
+function makeRows( tableData, options ) {
+	const { cellElement, rowElement, headingElement, wrappingElement, enforceWrapping, asWidget } = options;
+
 	return tableData
 		.reduce( ( previousRowsString, tableRow ) => {
 			const tableRowString = tableRow.reduce( ( tableRowString, tableCellData ) => {
-				let tableCell = tableCellData;
-
 				const isObject = typeof tableCellData === 'object';
 
+				let contents = isObject ? tableCellData.contents : tableCellData;
+
 				let resultingCellElement = cellElement;
 
 				if ( isObject ) {
-					tableCell = tableCellData.contents;
-
 					if ( tableCellData.isHeading ) {
 						resultingCellElement = headingElement;
 					}
@@ -156,8 +261,19 @@ function makeRows( tableData, cellElement, rowElement, headingElement = 'th' ) {
 					delete tableCellData.isHeading;
 				}
 
-				const formattedAttributes = formatAttributes( isObject ? tableCellData : '' );
-				tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ tableCell }</${ resultingCellElement }>`;
+				const attributes = isObject ? tableCellData : {};
+
+				if ( asWidget ) {
+					attributes.class = 'ck-editor__editable ck-editor__nested-editable';
+					attributes.contenteditable = 'true';
+				}
+
+				if ( !( contents.replace( '[', '' ).replace( ']', '' ).startsWith( '<' ) ) && enforceWrapping ) {
+					contents = `<${ wrappingElement }>${ contents }</${ wrappingElement }>`;
+				}
+
+				const formattedAttributes = formatAttributes( attributes );
+				tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ contents }</${ resultingCellElement }>`;
 
 				return tableRowString;
 			}, '' );

+ 32 - 58
packages/ckeditor5-table/tests/commands/insertcolumncommand.js

@@ -4,20 +4,10 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import InsertColumnCommand from '../../src/commands/insertcolumncommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 
 describe( 'InsertColumnCommand', () => {
@@ -32,48 +22,8 @@ describe( 'InsertColumnCommand', () => {
 				editor = newEditor;
 				model = editor.model;
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -88,7 +38,7 @@ describe( 'InsertColumnCommand', () => {
 
 		describe( 'isEnabled', () => {
 			it( 'should be false if wrong node', () => {
-				setData( model, '<p>foo[]</p>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 				expect( command.isEnabled ).to.be.false;
 			} );
 
@@ -99,7 +49,7 @@ describe( 'InsertColumnCommand', () => {
 		} );
 
 		describe( 'execute()', () => {
-			it( 'should insert column in given table at given index', () => {
+			it( 'should insert column in given table after selection\'s column', () => {
 				setData( model, modelTable( [
 					[ '11[]', '12' ],
 					[ '21', '22' ]
@@ -113,6 +63,18 @@ describe( 'InsertColumnCommand', () => {
 				] ) );
 			} );
 
+			it( 'should insert column in given table after selection\'s column (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '11', '<paragraph>12[]</paragraph>', '13' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '<paragraph>12[]</paragraph>', '', '13' ]
+				] ) );
+			} );
+
 			it( 'should insert columns at table end', () => {
 				setData( model, modelTable( [
 					[ '11', '12' ],
@@ -200,7 +162,7 @@ describe( 'InsertColumnCommand', () => {
 
 		describe( 'isEnabled', () => {
 			it( 'should be false if wrong node', () => {
-				setData( model, '<p>foo[]</p>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 				expect( command.isEnabled ).to.be.false;
 			} );
 
@@ -211,7 +173,7 @@ describe( 'InsertColumnCommand', () => {
 		} );
 
 		describe( 'execute()', () => {
-			it( 'should insert column in given table at given index', () => {
+			it( 'should insert column in given table before selection\'s column', () => {
 				setData( model, modelTable( [
 					[ '11', '12[]' ],
 					[ '21', '22' ]
@@ -225,6 +187,18 @@ describe( 'InsertColumnCommand', () => {
 				] ) );
 			} );
 
+			it( 'should insert column in given table before selection\'s column (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '11', '<paragraph>12[]</paragraph>', '13' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11', '', '<paragraph>12[]</paragraph>', '13' ]
+				] ) );
+			} );
+
 			it( 'should insert columns at the table start', () => {
 				setData( model, modelTable( [
 					[ '11', '12' ],

+ 40 - 56
packages/ckeditor5-table/tests/commands/insertrowcommand.js

@@ -4,20 +4,10 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import InsertRowCommand from '../../src/commands/insertrowcommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 
 describe( 'InsertRowCommand', () => {
@@ -32,48 +22,8 @@ describe( 'InsertRowCommand', () => {
 				editor = newEditor;
 				model = editor.model;
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -88,7 +38,7 @@ describe( 'InsertRowCommand', () => {
 
 		describe( 'isEnabled', () => {
 			it( 'should be false if wrong node', () => {
-				setData( model, '<p>foo[]</p>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 				expect( command.isEnabled ).to.be.false;
 			} );
 
@@ -114,6 +64,23 @@ describe( 'InsertRowCommand', () => {
 				] ) );
 			} );
 
+			it( 'should insert row after current position (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '00' ],
+					[ '<paragraph>[]10</paragraph>' ],
+					[ '20' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '00' ],
+					[ '<paragraph>[]10</paragraph>' ],
+					[ '' ],
+					[ '20' ]
+				] ) );
+			} );
+
 			it( 'should update table heading rows attribute when inserting row in headings section', () => {
 				setData( model, modelTable( [
 					[ '00[]', '01' ],
@@ -223,7 +190,7 @@ describe( 'InsertRowCommand', () => {
 
 		describe( 'isEnabled', () => {
 			it( 'should be false if wrong node', () => {
-				setData( model, '<p>foo[]</p>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 				expect( command.isEnabled ).to.be.false;
 			} );
 
@@ -234,6 +201,23 @@ describe( 'InsertRowCommand', () => {
 		} );
 
 		describe( 'execute()', () => {
+			it( 'should insert row before current position (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '00' ],
+					[ '<paragraph>[]10</paragraph>' ],
+					[ '20' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '00' ],
+					[ '' ],
+					[ '<paragraph>[]10</paragraph>' ],
+					[ '20' ]
+				] ) );
+			} );
+
 			it( 'should insert row at the beginning of a table', () => {
 				setData( model, modelTable( [
 					[ '00[]', '01' ],

+ 11 - 61
packages/ckeditor5-table/tests/commands/inserttablecommand.js

@@ -4,22 +4,12 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import InsertTableCommand from '../../src/commands/inserttablecommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
 import TableUtils from '../../src/tableutils';
 
-import { formatTable, formattedModelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable } from '../_utils/utils';
 
 describe( 'InsertTableCommand', () => {
 	let editor, model, command;
@@ -34,48 +24,8 @@ describe( 'InsertTableCommand', () => {
 				model = editor.model;
 				command = new InsertTableCommand( editor );
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -86,12 +36,12 @@ describe( 'InsertTableCommand', () => {
 	describe( 'isEnabled', () => {
 		describe( 'when selection is collapsed', () => {
 			it( 'should be true if in paragraph', () => {
-				setData( model, '<p>foo[]</p>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 				expect( command.isEnabled ).to.be.true;
 			} );
 
 			it( 'should be false if in table', () => {
-				setData( model, '<table><tableRow><tableCell>foo[]</tableCell></tableRow></table>' );
+				setData( model, '<table><tableRow><tableCell><paragraph>foo[]</paragraph></tableCell></tableRow></table>' );
 				expect( command.isEnabled ).to.be.false;
 			} );
 		} );
@@ -99,7 +49,7 @@ describe( 'InsertTableCommand', () => {
 
 	describe( 'execute()', () => {
 		it( 'should create a single batch', () => {
-			setData( model, '<p>foo[]</p>' );
+			setData( model, '<paragraph>foo[]</paragraph>' );
 
 			const spy = sinon.spy();
 
@@ -123,12 +73,12 @@ describe( 'InsertTableCommand', () => {
 			} );
 
 			it( 'should insert table with two rows and two columns after non-empty paragraph', () => {
-				setData( model, '<p>foo[]</p>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 
 				command.execute();
 
 				expect( formatTable( getData( model ) ) ).to.equal(
-					'<p>foo</p>' +
+					'<paragraph>foo</paragraph>' +
 					formattedModelTable( [
 						[ '[]', '' ],
 						[ '', '' ]
@@ -137,12 +87,12 @@ describe( 'InsertTableCommand', () => {
 			} );
 
 			it( 'should insert table with given rows and columns after non-empty paragraph', () => {
-				setData( model, '<p>foo[]</p>' );
+				setData( model, '<paragraph>foo[]</paragraph>' );
 
 				command.execute( { rows: 3, columns: 4 } );
 
 				expect( formatTable( getData( model ) ) ).to.equal(
-					'<p>foo</p>' +
+					'<paragraph>foo</paragraph>' +
 					formattedModelTable( [
 						[ '[]', '', '', '' ],
 						[ '', '', '', '' ],

+ 295 - 73
packages/ckeditor5-table/tests/commands/mergecellcommand.js

@@ -5,19 +5,9 @@
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
 import MergeCellCommand from '../../src/commands/mergecellcommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 
 describe( 'MergeCellCommand', () => {
@@ -33,48 +23,8 @@ describe( 'MergeCellCommand', () => {
 				model = editor.model;
 				root = model.document.getRoot( 'main' );
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -139,7 +89,7 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be false if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.isEnabled ).to.be.false;
 			} );
@@ -154,6 +104,14 @@ describe( 'MergeCellCommand', () => {
 				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) );
 			} );
 
+			it( 'should be set to mergeable sibling if in cell that has sibling on the right (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '00', '<paragraph>[]01</paragraph>', '02' ]
+				] ) );
+
+				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 2 ] ) );
+			} );
+
 			it( 'should be undefined if last cell of a row', () => {
 				setData( model, modelTable( [
 					[ '00', '01[]' ]
@@ -179,14 +137,14 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.value ).to.be.undefined;
 			} );
 		} );
 
 		describe( 'execute()', () => {
-			it( 'should merge table cells ', () => {
+			it( 'should merge table cells', () => {
 				setData( model, modelTable( [
 					[ '[]00', '01' ]
 				] ) );
@@ -194,7 +152,61 @@ describe( 'MergeCellCommand', () => {
 				command.execute();
 
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '[0001]' } ]
+					[ { colspan: 2, contents: '<paragraph>[00</paragraph><paragraph>01]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should result in single empty paragraph if both cells are empty', () => {
+				setData( model, modelTable( [
+					[ '[]', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<paragraph>[]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (other cell is empty)', () => {
+				setData( model, modelTable( [
+					[ 'foo[]', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (selection cell is empty)', () => {
+				setData( model, modelTable( [
+					[ '[]', 'foo' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should not merge other empty blocks to single block', () => {
+				model.schema.register( 'block', {
+					allowWhere: '$block',
+					allowContentOf: '$block',
+					isBlock: true
+				} );
+
+				setData( model, modelTable( [
+					[ '<block>[]</block>', '<block></block>' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<block>[</block><block>]</block>' } ]
 				] ) );
 			} );
 		} );
@@ -257,7 +269,7 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be false if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.isEnabled ).to.be.false;
 			} );
@@ -272,6 +284,14 @@ describe( 'MergeCellCommand', () => {
 				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
 			} );
 
+			it( 'should be set to mergeable sibling if in cell that has sibling on the left (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '00', '<paragraph>01[]</paragraph>', '02' ]
+				] ) );
+
+				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
+			} );
+
 			it( 'should be undefined if first cell of a row', () => {
 				setData( model, modelTable( [
 					[ '00[]', '01' ]
@@ -297,14 +317,14 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.value ).to.be.undefined;
 			} );
 		} );
 
 		describe( 'execute()', () => {
-			it( 'should merge table cells ', () => {
+			it( 'should merge table cells', () => {
 				setData( model, modelTable( [
 					[ '00', '[]01' ]
 				] ) );
@@ -312,7 +332,61 @@ describe( 'MergeCellCommand', () => {
 				command.execute();
 
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '[0001]' } ]
+					[ { colspan: 2, contents: '<paragraph>[00</paragraph><paragraph>01]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should result in single empty paragraph if both cells are empty', () => {
+				setData( model, modelTable( [
+					[ '', '[]' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<paragraph>[]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (other cell is empty)', () => {
+				setData( model, modelTable( [
+					[ '', 'foo[]' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (selection cell is empty)', () => {
+				setData( model, modelTable( [
+					[ 'foo', '[]' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
+				] ) );
+			} );
+
+			it( 'should not merge other empty blocks to single block', () => {
+				model.schema.register( 'block', {
+					allowWhere: '$block',
+					allowContentOf: '$block',
+					isBlock: true
+				} );
+
+				setData( model, modelTable( [
+					[ '<block></block>', '<block>[]</block>' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { colspan: 2, contents: '<block>[</block><block>]</block>' } ]
 				] ) );
 			} );
 		} );
@@ -361,7 +435,7 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be false if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.isEnabled ).to.be.false;
 			} );
@@ -386,6 +460,16 @@ describe( 'MergeCellCommand', () => {
 				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 1, 1 ] ) );
 			} );
 
+			it( 'should be set to mergeable cell (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '00' ],
+					[ '<paragraph>10[]</paragraph>' ],
+					[ '20' ]
+				] ) );
+
+				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 2, 0 ] ) );
+			} );
+
 			it( 'should be undefined if in last row', () => {
 				setData( model, modelTable( [
 					[ '00', '01' ],
@@ -424,14 +508,14 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.value ).to.be.undefined;
 			} );
 		} );
 
 		describe( 'execute()', () => {
-			it( 'should merge table cells ', () => {
+			it( 'should merge table cells', () => {
 				setData( model, modelTable( [
 					[ '00', '01[]' ],
 					[ '10', '11' ]
@@ -440,11 +524,73 @@ describe( 'MergeCellCommand', () => {
 				command.execute();
 
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', { rowspan: 2, contents: '[0111]' } ],
+					[ '00', { rowspan: 2, contents: '<paragraph>[01</paragraph><paragraph>11]</paragraph>' } ],
 					[ '10' ]
 				] ) );
 			} );
 
+			it( 'should result in single empty paragraph if both cells are empty', () => {
+				setData( model, modelTable( [
+					[ '[]', '' ],
+					[ '', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '[]' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (other cell is empty)', () => {
+				setData( model, modelTable( [
+					[ 'foo[]', '' ],
+					[ '', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '[foo]' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (selection cell is empty)', () => {
+				setData( model, modelTable( [
+					[ '[]', '' ],
+					[ 'foo', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '[foo]' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
+			it( 'should not merge other empty blocks to single block', () => {
+				model.schema.register( 'block', {
+					allowWhere: '$block',
+					allowContentOf: '$block',
+					isBlock: true
+				} );
+
+				setData( model, modelTable( [
+					[ '<block>[]</block>', '' ],
+					[ '<block></block>', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '<block>[</block><block>]</block>' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
 			it( 'should remove empty row if merging table cells ', () => {
 				setData( model, modelTable( [
 					[ { rowspan: 2, contents: '00' }, '01[]', { rowspan: 3, contents: '02' } ],
@@ -455,7 +601,7 @@ describe( 'MergeCellCommand', () => {
 				command.execute();
 
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', '[0111]', { rowspan: 2, contents: '02' } ],
+					[ '00', '<paragraph>[01</paragraph><paragraph>11]</paragraph>', { rowspan: 2, contents: '02' } ],
 					[ '20', '21' ]
 				] ) );
 			} );
@@ -474,7 +620,7 @@ describe( 'MergeCellCommand', () => {
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 					[ { rowspan: 2, contents: '00' }, '01', '02' ],
 					[ '11', '12' ],
-					[ '20', '[2131]', { rowspan: 2, contents: '22' } ],
+					[ '20', '<paragraph>[21</paragraph><paragraph>31]</paragraph>', { rowspan: 2, contents: '22' } ],
 					[ '40', '41' ]
 				] ) );
 			} );
@@ -524,7 +670,7 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be false if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.isEnabled ).to.be.false;
 			} );
@@ -549,6 +695,16 @@ describe( 'MergeCellCommand', () => {
 				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) );
 			} );
 
+			it( 'should be set to mergeable cell (selection in block content)', () => {
+				setData( model, modelTable( [
+					[ '00' ],
+					[ '<paragraph>10[]</paragraph>' ],
+					[ '20' ]
+				] ) );
+
+				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
+			} );
+
 			it( 'should be undefined if in first row', () => {
 				setData( model, modelTable( [
 					[ '00[]', '01' ],
@@ -588,7 +744,7 @@ describe( 'MergeCellCommand', () => {
 			} );
 
 			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.value ).to.be.undefined;
 			} );
@@ -604,11 +760,73 @@ describe( 'MergeCellCommand', () => {
 				command.execute();
 
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', { rowspan: 2, contents: '[0111]' } ],
+					[ '00', { rowspan: 2, contents: '<paragraph>[01</paragraph><paragraph>11]</paragraph>' } ],
 					[ '10' ]
 				] ) );
 			} );
 
+			it( 'should result in single empty paragraph if both cells are empty', () => {
+				setData( model, modelTable( [
+					[ '', '' ],
+					[ '[]', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '[]' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (other cell is empty)', () => {
+				setData( model, modelTable( [
+					[ '', '' ],
+					[ 'foo[]', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '[foo]' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
+			it( 'should result in single paragraph (selection cell is empty)', () => {
+				setData( model, modelTable( [
+					[ 'foo', '' ],
+					[ '[]', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '[foo]' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
+			it( 'should not merge other empty blocks to single block', () => {
+				model.schema.register( 'block', {
+					allowWhere: '$block',
+					allowContentOf: '$block',
+					isBlock: true
+				} );
+
+				setData( model, modelTable( [
+					[ '<block></block>', '' ],
+					[ '<block>[]</block>', '' ]
+				] ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ { rowspan: 2, contents: '<block>[</block><block>]</block>' }, '' ],
+					[ '' ]
+				] ) );
+			} );
+
 			it( 'should properly merge cells in rows with spaned cells', () => {
 				setData( model, modelTable( [
 					[ { rowspan: 3, contents: '00' }, '11', '12', '13' ],
@@ -621,7 +839,11 @@ describe( 'MergeCellCommand', () => {
 
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 					[ { rowspan: 3, contents: '00' }, '11', '12', '13' ],
-					[ { rowspan: 2, contents: '21' }, '22', { rowspan: 3, contents: '[2333]' } ],
+					[
+						{ rowspan: 2, contents: '21' },
+						'22',
+						{ rowspan: 3, contents: '<paragraph>[23</paragraph><paragraph>33]</paragraph>' }
+					],
 					[ '32' ],
 					[ { colspan: 2, contents: '40' }, '42' ]
 				] ) );
@@ -637,7 +859,7 @@ describe( 'MergeCellCommand', () => {
 				command.execute();
 
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', '[0111]', { rowspan: 2, contents: '02' } ],
+					[ '00', '<paragraph>[01</paragraph><paragraph>11]</paragraph>', { rowspan: 2, contents: '02' } ],
 					[ '20', '21' ]
 				] ) );
 			} );
@@ -656,7 +878,7 @@ describe( 'MergeCellCommand', () => {
 				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 					[ { rowspan: 2, contents: '00' }, '01', '02' ],
 					[ '11', '12' ],
-					[ '20', '[2131]', { rowspan: 2, contents: '22' } ],
+					[ '20', '<paragraph>[21</paragraph><paragraph>31]</paragraph>', { rowspan: 2, contents: '22' } ],
 					[ '40', '41' ]
 				] ) );
 			} );

+ 9 - 59
packages/ckeditor5-table/tests/commands/removecolumncommand.js

@@ -4,20 +4,10 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import RemoveColumnCommand from '../../src/commands/removecolumncommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 
 describe( 'RemoveColumnCommand', () => {
@@ -33,48 +23,8 @@ describe( 'RemoveColumnCommand', () => {
 				model = editor.model;
 				command = new RemoveColumnCommand( editor );
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows', 'headingColumns' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -103,7 +53,7 @@ describe( 'RemoveColumnCommand', () => {
 		} );
 
 		it( 'should be false if selection is outside a table', () => {
-			setData( model, '<p>11[]</p>' );
+			setData( model, '<paragraph>11[]</paragraph>' );
 
 			expect( command.isEnabled ).to.be.false;
 		} );
@@ -121,7 +71,7 @@ describe( 'RemoveColumnCommand', () => {
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ '00', '02' ],
-				[ '10[]', '12' ],
+				[ '<paragraph>10[]</paragraph>', '12' ],
 				[ '20', '22' ]
 			] ) );
 		} );
@@ -136,7 +86,7 @@ describe( 'RemoveColumnCommand', () => {
 			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '[]01' ],
+				[ '<paragraph>[]01</paragraph>' ],
 				[ '11' ],
 				[ '21' ]
 			] ) );
@@ -153,7 +103,7 @@ describe( 'RemoveColumnCommand', () => {
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ '01' ],
-				[ '[]11' ],
+				[ '<paragraph>[]11</paragraph>' ],
 				[ '21' ]
 			], { headingColumns: 1 } ) );
 		} );
@@ -172,7 +122,7 @@ describe( 'RemoveColumnCommand', () => {
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ { colspan: 3, contents: '00' }, '03' ],
 				[ { colspan: 2, contents: '10' }, '13' ],
-				[ { colspan: 2, contents: '20[]' }, '23' ],
+				[ { colspan: 2, contents: '<paragraph>20[]</paragraph>' }, '23' ],
 				[ '30', '31', '33' ],
 				[ '40', '41', '43' ]
 

+ 9 - 59
packages/ckeditor5-table/tests/commands/removerowcommand.js

@@ -4,20 +4,10 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import RemoveRowCommand from '../../src/commands/removerowcommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 
 describe( 'RemoveRowCommand', () => {
 	let editor, model, command;
@@ -29,48 +19,8 @@ describe( 'RemoveRowCommand', () => {
 				model = editor.model;
 				command = new RemoveRowCommand( editor );
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -97,7 +47,7 @@ describe( 'RemoveRowCommand', () => {
 		} );
 
 		it( 'should be false if selection is outside a table', () => {
-			setData( model, '<p>11[]</p>' );
+			setData( model, '<paragraph>11[]</paragraph>' );
 
 			expect( command.isEnabled ).to.be.false;
 		} );
@@ -114,7 +64,7 @@ describe( 'RemoveRowCommand', () => {
 			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '00', '01[]' ],
+				[ '00', '<paragraph>01[]</paragraph>' ],
 				[ '20', '21' ]
 			] ) );
 		} );
@@ -129,7 +79,7 @@ describe( 'RemoveRowCommand', () => {
 			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '[]10', '11' ],
+				[ '<paragraph>[]10</paragraph>', '11' ],
 				[ '20', '21' ]
 			] ) );
 		} );
@@ -144,7 +94,7 @@ describe( 'RemoveRowCommand', () => {
 			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '00', '01[]' ],
+				[ '00', '<paragraph>01[]</paragraph>' ],
 				[ '20', '21' ]
 			], { headingRows: 1 } ) );
 		} );
@@ -161,7 +111,7 @@ describe( 'RemoveRowCommand', () => {
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
-				[ '13', '14[]' ],
+				[ '13', '<paragraph>14[]</paragraph>' ],
 				[ '30', '31', '32', '33', '34' ]
 			] ) );
 		} );

+ 7 - 64
packages/ckeditor5-table/tests/commands/setheadercolumncommand.js

@@ -4,23 +4,13 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import SetHeaderColumnCommand from '../../src/commands/setheadercolumncommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 
-describe( 'HeaderColumnCommand', () => {
+describe( 'SetHeaderColumnCommand', () => {
 	let editor, model, command;
 
 	beforeEach( () => {
@@ -33,55 +23,8 @@ describe( 'HeaderColumnCommand', () => {
 				model = editor.model;
 				command = new SetHeaderColumnCommand( editor );
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					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
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -91,12 +34,12 @@ describe( 'HeaderColumnCommand', () => {
 
 	describe( 'isEnabled', () => {
 		it( 'should be false if selection is not in a table', () => {
-			setData( model, '<p>foo[]</p>' );
+			setData( model, '<paragraph>foo[]</paragraph>' );
 			expect( command.isEnabled ).to.be.false;
 		} );
 
 		it( 'should be true if selection is in table', () => {
-			setData( model, '<table><tableRow><tableCell>foo[]</tableCell></tableRow></table>' );
+			setData( model, '<table><tableRow><tableCell><paragraph>foo[]</paragraph></tableCell></tableRow></table>' );
 			expect( command.isEnabled ).to.be.true;
 		} );
 	} );

+ 7 - 58
packages/ckeditor5-table/tests/commands/setheaderrowcommand.js

@@ -4,23 +4,12 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import SetHeaderRowCommand from '../../src/commands/setheaderrowcommand';
-
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 
-describe( 'HeaderRowCommand', () => {
+describe( 'SetHeaderRowCommand', () => {
 	let editor, model, command;
 
 	beforeEach( () => {
@@ -33,48 +22,8 @@ describe( 'HeaderRowCommand', () => {
 				model = editor.model;
 				command = new SetHeaderRowCommand( editor );
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -84,12 +33,12 @@ describe( 'HeaderRowCommand', () => {
 
 	describe( 'isEnabled', () => {
 		it( 'should be false if selection is not in a table', () => {
-			setData( model, '<p>foo[]</p>' );
+			setData( model, '<paragraph>foo[]</paragraph>' );
 			expect( command.isEnabled ).to.be.false;
 		} );
 
 		it( 'should be true if selection is in table', () => {
-			setData( model, '<table><tableRow><tableCell>foo[]</tableCell></tableRow></table>' );
+			setData( model, '<table><tableRow><tableCell><paragraph>foo[]</paragraph></tableCell></tableRow></table>' );
 			expect( command.isEnabled ).to.be.true;
 		} );
 	} );

+ 6 - 56
packages/ckeditor5-table/tests/commands/splitcellcommand.js

@@ -4,20 +4,10 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import SplitCellCommand from '../../src/commands/splitcellcommand';
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 
 describe( 'SplitCellCommand', () => {
@@ -33,48 +23,8 @@ describe( 'SplitCellCommand', () => {
 				model = editor.model;
 				command = new SplitCellCommand( editor );
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -97,7 +47,7 @@ describe( 'SplitCellCommand', () => {
 			} );
 
 			it( 'should be false if not in cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.isEnabled ).to.be.false;
 			} );
@@ -201,7 +151,7 @@ describe( 'SplitCellCommand', () => {
 			} );
 
 			it( 'should be false if not in cell', () => {
-				setData( model, '<p>11[]</p>' );
+				setData( model, '<paragraph>11[]</paragraph>' );
 
 				expect( command.isEnabled ).to.be.false;
 			} );

+ 8 - 41
packages/ckeditor5-table/tests/commands/utils.js

@@ -5,12 +5,10 @@
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
-import { downcastInsertTable } from '../../src/converters/downcast';
-import upcastTable from '../../src/converters/upcasttable';
-import { modelTable } from '../_utils/utils';
-import { getParentTable } from '../../src/commands/utils';
+import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+
+import { findAncestor } from '../../src/commands/utils';
 
 describe( 'commands utils', () => {
 	let editor, model;
@@ -21,39 +19,8 @@ describe( 'commands utils', () => {
 				editor = newEditor;
 				model = editor.model;
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-				// Table row upcast only since downcast conversion is done in `downcastTable()`.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
-
-				// Table cell conversion.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -63,15 +30,15 @@ describe( 'commands utils', () => {
 
 	describe( 'getParentTable()', () => {
 		it( 'should return undefined if not in table', () => {
-			setData( model, '<p>foo[]</p>' );
+			setData( model, '<paragraph>foo[]</paragraph>' );
 
-			expect( getParentTable( model.document.selection.focus ) ).to.be.undefined;
+			expect( findAncestor( 'table', model.document.selection.focus ) ).to.be.undefined;
 		} );
 
 		it( 'should return table if position is in tableCell', () => {
 			setData( model, modelTable( [ [ '[]' ] ] ) );
 
-			const parentTable = getParentTable( model.document.selection.focus );
+			const parentTable = findAncestor( 'table', model.document.selection.focus );
 
 			expect( parentTable ).to.not.be.undefined;
 			expect( parentTable.is( 'table' ) ).to.be.true;

+ 143 - 178
packages/ckeditor5-table/tests/converters/downcast.js

@@ -7,18 +7,32 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../../src/converters/downcast';
-import { formatTable, formattedViewTable, modelTable } from '../_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, formattedViewTable, modelTable } from '../_utils/utils';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
+function paragraphInTableCell() {
+	return dispatcher => dispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+		const tableCell = data.item.parent;
+
+		if ( tableCell.is( 'tableCell' ) && tableCell.childCount > 1 ) {
+			for ( const child of tableCell.getChildren() ) {
+				if ( child.name != 'paragraph' ) {
+					continue;
+				}
+
+				const viewElement = conversionApi.mapper.toViewElement( child );
+
+				if ( viewElement && viewElement.name === 'span' ) {
+					conversionApi.mapper.unbindModelElement( tableCell );
+					conversionApi.writer.rename( viewElement, 'p' );
+					conversionApi.mapper.bindElements( child, viewElement );
+				}
+			}
+		}
+	}, { converterPriority: 'highest' } );
+}
+
 describe( 'downcast converters', () => {
 	let editor, model, doc, root, viewDocument;
 
@@ -36,36 +50,8 @@ describe( 'downcast converters', () => {
 				root = doc.getRoot( 'main' );
 				viewDocument = editor.editing.view;
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows', 'headingColumns' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-				// Insert conversion
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -142,6 +128,25 @@ describe( 'downcast converters', () => {
 			) );
 		} );
 
+		it( 'should create table with block content', () => {
+			setModelData( model, modelTable( [
+				[ '<paragraph>00</paragraph><paragraph>foo</paragraph>', '01' ]
+			] ) );
+
+			expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
+				'<figure class="table">' +
+					'<table>' +
+						'<tbody>' +
+							'<tr>' +
+								'<td><p>00</p><p>foo</p></td>' +
+								'<td>01</td>' +
+							'</tr>' +
+						'</tbody>' +
+					'</table>' +
+				'</figure>'
+			) );
+		} );
+
 		it( 'should be possible to overwrite', () => {
 			editor.conversion.elementToElement( { model: 'tableRow', view: 'tr', converterPriority: 'high' } );
 			editor.conversion.elementToElement( { model: 'tableCell', view: 'td', converterPriority: 'high' } );
@@ -161,7 +166,7 @@ describe( 'downcast converters', () => {
 
 			expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
 				'<table foo="bar">' +
-					'<tr><td></td></tr>' +
+					'<tr><td><p></p></td></tr>' +
 				'</table>'
 			) );
 		} );
@@ -240,7 +245,7 @@ describe( 'downcast converters', () => {
 			} );
 		} );
 
-		describe( 'asWidget', () => {
+		describe( 'options.asWidget=true', () => {
 			beforeEach( () => {
 				return VirtualTestEditor.create()
 					.then( newEditor => {
@@ -250,30 +255,8 @@ describe( 'downcast converters', () => {
 						root = doc.getRoot( 'main' );
 						viewDocument = editor.editing.view;
 
-						const conversion = editor.conversion;
-						const schema = model.schema;
-
-						schema.register( 'table', {
-							allowWhere: '$block',
-							allowAttributes: [ 'headingRows', 'headingColumns' ],
-							isObject: true
-						} );
-
-						schema.register( 'tableRow', { allowIn: 'table' } );
-
-						schema.register( 'tableCell', {
-							allowIn: 'tableRow',
-							allowContentOf: '$block',
-							allowAttributes: [ 'colspan', 'rowspan' ],
-							isLimit: true
-						} );
-
-						conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
-
-						conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-						conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+						defaultSchema( model.schema );
+						defaultConversion( editor.conversion, true );
 					} );
 			} );
 
@@ -285,7 +268,9 @@ describe( 'downcast converters', () => {
 					'<div class="ck ck-widget__selection-handler"></div>' +
 						'<table>' +
 							'<tbody>' +
-								'<tr><td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td></tr>' +
+								'<tr>' +
+									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"><span></span></td>' +
+								'</tr>' +
 							'</tbody>' +
 						'</table>' +
 					'</figure>'
@@ -483,7 +468,7 @@ describe( 'downcast converters', () => {
 			] ) );
 		} );
 
-		describe( 'asWidget', () => {
+		describe( 'options.asWidget=true', () => {
 			beforeEach( () => {
 				return VirtualTestEditor.create()
 					.then( newEditor => {
@@ -493,30 +478,8 @@ describe( 'downcast converters', () => {
 						root = doc.getRoot( 'main' );
 						viewDocument = editor.editing.view;
 
-						const conversion = editor.conversion;
-						const schema = model.schema;
-
-						schema.register( 'table', {
-							allowWhere: '$block',
-							allowAttributes: [ 'headingRows', 'headingColumns' ],
-							isObject: true
-						} );
-
-						schema.register( 'tableRow', { allowIn: 'table' } );
-
-						schema.register( 'tableCell', {
-							allowIn: 'tableRow',
-							allowContentOf: '$block',
-							allowAttributes: [ 'colspan', 'rowspan' ],
-							isLimit: true
-						} );
-
-						conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
-
-						conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-						conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+						defaultSchema( model.schema );
+						defaultConversion( editor.conversion, true );
 					} );
 			} );
 
@@ -532,13 +495,20 @@ describe( 'downcast converters', () => {
 					writer.insert( writer.createElement( 'tableCell' ), firstRow, 'end' );
 				} );
 
-				expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
+				expect( formatTable(
+					getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formatTable(
 					'<figure class="ck-widget ck-widget_selectable table" contenteditable="false">' +
 						'<div class="ck ck-widget__selection-handler"></div>' +
 						'<table>' +
 							'<tbody>' +
-								'<tr><td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</td></tr>' +
-								'<tr><td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td></tr>' +
+								'<tr>' +
+									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
+										'<span>00</span>' +
+									'</td>' +
+								'</tr>' +
+								'<tr>' +
+									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td>' +
+								'</tr>' +
 							'</tbody>' +
 						'</table>' +
 					'</figure>'
@@ -646,7 +616,7 @@ describe( 'downcast converters', () => {
 			] ) );
 		} );
 
-		describe( 'asWidget', () => {
+		describe( 'options.asWidget=true', () => {
 			beforeEach( () => {
 				return VirtualTestEditor.create()
 					.then( newEditor => {
@@ -656,30 +626,8 @@ describe( 'downcast converters', () => {
 						root = doc.getRoot( 'main' );
 						viewDocument = editor.editing.view;
 
-						const conversion = editor.conversion;
-						const schema = model.schema;
-
-						schema.register( 'table', {
-							allowWhere: '$block',
-							allowAttributes: [ 'headingRows', 'headingColumns' ],
-							isObject: true
-						} );
-
-						schema.register( 'tableRow', { allowIn: 'table' } );
-
-						schema.register( 'tableCell', {
-							allowIn: 'tableRow',
-							allowContentOf: '$block',
-							allowAttributes: [ 'colspan', 'rowspan' ],
-							isLimit: true
-						} );
-
-						conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
-
-						conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-						conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+						defaultSchema( model.schema );
+						defaultConversion( editor.conversion, true );
 					} );
 			} );
 
@@ -700,7 +648,9 @@ describe( 'downcast converters', () => {
 						'<table>' +
 							'<tbody>' +
 								'<tr>' +
-									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</td>' +
+					'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
+					'<span>00</span>' +
+					'</td>' +
 									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true"></td>' +
 								'</tr>' +
 							'</tbody>' +
@@ -845,7 +795,7 @@ describe( 'downcast converters', () => {
 			] ) );
 		} );
 
-		describe( 'asWidget', () => {
+		describe( 'options.asWidget=true', () => {
 			beforeEach( () => {
 				return VirtualTestEditor.create()
 					.then( newEditor => {
@@ -855,33 +805,8 @@ describe( 'downcast converters', () => {
 						root = doc.getRoot( 'main' );
 						viewDocument = editor.editing.view;
 
-						const conversion = editor.conversion;
-						const schema = model.schema;
-
-						schema.register( 'table', {
-							allowWhere: '$block',
-							allowAttributes: [ 'headingRows', 'headingColumns' ],
-							isObject: true
-						} );
-
-						schema.register( 'tableRow', { allowIn: 'table' } );
-
-						schema.register( 'tableCell', {
-							allowIn: 'tableRow',
-							allowContentOf: '$block',
-							allowAttributes: [ 'colspan', 'rowspan' ],
-							isLimit: true
-						} );
-
-						conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
-
-						conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
-
-						conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-						conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+						defaultSchema( model.schema );
+						defaultConversion( editor.conversion, true );
 					} );
 			} );
 
@@ -899,7 +824,11 @@ describe( 'downcast converters', () => {
 					'<div class="ck ck-widget__selection-handler"></div>' +
 						'<table>' +
 							'<thead>' +
-								'<tr><th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</th></tr>' +
+								'<tr>' +
+									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
+										'<span>00</span>' +
+									'</th>' +
+								'</tr>' +
 							'</thead>' +
 						'</table>' +
 					'</figure>'
@@ -1080,7 +1009,7 @@ describe( 'downcast converters', () => {
 			], { headingRows: 2 } ) );
 		} );
 
-		describe( 'asWidget', () => {
+		describe( 'options.asWidget=true', () => {
 			beforeEach( () => {
 				return VirtualTestEditor.create()
 					.then( newEditor => {
@@ -1090,33 +1019,8 @@ describe( 'downcast converters', () => {
 						root = doc.getRoot( 'main' );
 						viewDocument = editor.editing.view;
 
-						const conversion = editor.conversion;
-						const schema = model.schema;
-
-						schema.register( 'table', {
-							allowWhere: '$block',
-							allowAttributes: [ 'headingRows', 'headingColumns' ],
-							isObject: true
-						} );
-
-						schema.register( 'tableRow', { allowIn: 'table' } );
-
-						schema.register( 'tableCell', {
-							allowIn: 'tableRow',
-							allowContentOf: '$block',
-							allowAttributes: [ 'colspan', 'rowspan' ],
-							isLimit: true
-						} );
-
-						conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget: true } ) );
-
-						conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget: true } ) );
-						conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget: true } ) );
-
-						conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-						conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+						defaultSchema( model.schema );
+						defaultConversion( editor.conversion, true );
 					} );
 			} );
 
@@ -1134,7 +1038,11 @@ describe( 'downcast converters', () => {
 					'<div class="ck ck-widget__selection-handler"></div>' +
 						'<table>' +
 							'<tbody>' +
-								'<tr><th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">00</th></tr>' +
+								'<tr>' +
+									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true">' +
+										'<span>00</span>' +
+									'</th>' +
+								'</tr>' +
 							'</tbody>' +
 						'</table>' +
 					'</figure>'
@@ -1247,4 +1155,61 @@ describe( 'downcast converters', () => {
 			], { headingRows: 1 } ) );
 		} );
 	} );
+
+	describe( 'options.asWidget=true', () => {
+		beforeEach( () => {
+			return VirtualTestEditor.create()
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					doc = model.document;
+					root = doc.getRoot( 'main' );
+					viewDocument = editor.editing.view;
+
+					defaultSchema( model.schema );
+					defaultConversion( editor.conversion, true );
+
+					editor.conversion.for( 'downcast' ).add( paragraphInTableCell() );
+				} );
+		} );
+
+		it( 'should rename <span> to <p> when more then one block content inside table cell', () => {
+			setModelData( model, modelTable( [
+				[ '00[]' ]
+			] ) );
+
+			expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+				[ '00' ]
+			], { asWidget: true } ) );
+
+			const table = root.getChild( 0 );
+
+			model.change( writer => {
+				const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
+
+				const paragraph = writer.createElement( 'paragraph' );
+
+				writer.insert( paragraph, nodeByPath, 'after' );
+
+				writer.setSelection( nodeByPath.nextSibling, 0 );
+			} );
+
+			expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+				[ '<p>00</p><p></p>' ]
+			], { asWidget: true } ) );
+		} );
+
+		it( 'should rename <span> to <p> for single paragraph with attribute', () => {
+			model.schema.extend( '$block', { allowAttributes: 'foo' } );
+			editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } );
+
+			setModelData( model, modelTable( [
+				[ '<paragraph foo="bar">00[]</paragraph>' ]
+			] ) );
+
+			expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+				[ '<p foo="bar">00</p>' ]
+			], { asWidget: true } ) );
+		} );
+	} );
 } );

+ 5 - 2
packages/ckeditor5-table/tests/converters/table-post-fixer.js

@@ -421,7 +421,9 @@ describe( 'Table post-fixer', () => {
 			const table = root.getChild( 0 );
 			const tableRow = table.getChild( rowIndex );
 
-			writer.insertElement( 'tableCell', tableRow, index );
+			const tableCell = writer.createElement( 'tableCell' );
+			writer.insert( tableCell, tableRow, index );
+			writer.insertElement( 'paragraph', tableCell );
 		}
 
 		function _setAttribute( writer, attributeKey, attributeValue, path ) {
@@ -441,9 +443,10 @@ describe( 'Table post-fixer', () => {
 
 			for ( const index of rows ) {
 				const tableRow = table.getChild( index );
-				const tableCell = writer.createElement( 'tableCell' );
 
+				const tableCell = writer.createElement( 'tableCell' );
 				writer.insert( tableCell, tableRow, columnIndex );
+				writer.insertElement( 'paragraph', tableCell );
 			}
 		}
 	} );

+ 211 - 0
packages/ckeditor5-table/tests/converters/tablecell-post-fixer.js

@@ -0,0 +1,211 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { defaultConversion, defaultSchema, formatTable, formattedViewTable, modelTable } from '../_utils/utils';
+import injectTableCellPostFixer from '../../src/converters/tablecell-post-fixer';
+
+import env from '@ckeditor/ckeditor5-utils/src/env';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'TableCell post-fixer', () => {
+	let editor, model, doc, root, viewDocument;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		// Most tests assume non-edge environment but we do not set `contenteditable=false` on Edge so stub `env.isEdge`.
+		testUtils.sinon.stub( env, 'isEdge' ).get( () => false );
+
+		return VirtualTestEditor.create()
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+				root = doc.getRoot( 'main' );
+				viewDocument = editor.editing.view;
+
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion, true );
+
+				editor.model.schema.register( 'block', {
+					inheritAllFrom: '$block'
+				} );
+				editor.conversion.elementToElement( { model: 'block', view: 'div' } );
+
+				model.schema.extend( '$block', { allowAttributes: 'foo' } );
+				editor.conversion.attributeToAttribute( { model: 'foo', view: 'foo' } );
+
+				injectTableCellPostFixer( model, editor.editing );
+			} );
+	} );
+
+	it( 'should rename <span> to <p> when adding more <paragraph> elements to the same table cell', () => {
+		setModelData( model, modelTable( [ [ '00[]' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
+
+			const paragraph = writer.createElement( 'paragraph' );
+
+			writer.insert( paragraph, nodeByPath, 'after' );
+
+			writer.setSelection( nodeByPath.nextSibling, 0 );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p>00</p><p></p>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should rename <span> to <p> on adding other block element to the same table cell', () => {
+		setModelData( model, modelTable( [ [ '00[]' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
+
+			const paragraph = writer.createElement( 'block' );
+
+			writer.insert( paragraph, nodeByPath, 'after' );
+
+			writer.setSelection( nodeByPath.nextSibling, 0 );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p>00</p><div></div>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should properly rename the same element on consecutive changes', () => {
+		setModelData( model, modelTable( [ [ '00[]' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			const nodeByPath = table.getNodeByPath( [ 0, 0, 0 ] );
+
+			writer.insertElement( 'paragraph', nodeByPath, 'after' );
+
+			writer.setSelection( nodeByPath.nextSibling, 0 );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p>00</p><p></p>' ]
+		], { asWidget: true } ) );
+
+		model.change( writer => {
+			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '00' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should rename <span> to <p> when setting attribute on <paragraph>', () => {
+		setModelData( model, modelTable( [ [ '<paragraph>00[]</paragraph>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p foo="bar">00</p>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should rename <p> to <span> when removing all but one paragraph inside table cell', () => {
+		setModelData( model, modelTable( [ [ '<paragraph>00[]</paragraph><paragraph>foo</paragraph>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.remove( table.getNodeByPath( [ 0, 0, 1 ] ) );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '00' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should rename <p> to <span> when removing attribute from <paragraph>', () => {
+		setModelData( model, modelTable( [ [ '<paragraph foo="bar">00[]</paragraph>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.removeAttribute( 'foo', table.getNodeByPath( [ 0, 0, 0 ] ) );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<span>00</span>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should keep <p> in the view when <paragraph> attribute value is changed', () => {
+		setModelData( model, modelTable( [ [ '<paragraph foo="bar">00[]</paragraph>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p foo="baz">00</p>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should keep <p> in the view when <paragraph> attribute value is changed (table cell with multiple blocks)', () => {
+		setModelData( model, modelTable( [ [ '<paragraph foo="bar">00[]</paragraph><paragraph>00</paragraph>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.setAttribute( 'foo', 'baz', table.getNodeByPath( [ 0, 0, 0 ] ) );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<p foo="baz">00</p><p>00</p>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should do nothing on rename <paragraph> to other block', () => {
+		setModelData( model, modelTable( [ [ '00' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.rename( table.getNodeByPath( [ 0, 0, 0 ] ), 'block' );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<div>00</div>' ]
+		], { asWidget: true } ) );
+	} );
+
+	it( 'should do nothing when setting attribute on block item other then <paragraph>', () => {
+		setModelData( model, modelTable( [ [ '<block>foo</block>' ] ] ) );
+
+		const table = root.getChild( 0 );
+
+		model.change( writer => {
+			writer.setAttribute( 'foo', 'bar', table.getNodeByPath( [ 0, 0, 0 ] ) );
+		} );
+
+		expect( formatTable( getViewData( viewDocument, { withoutSelection: true } ) ) ).to.equal( formattedViewTable( [
+			[ '<div foo="bar">foo</div>' ]
+		], { asWidget: true } ) );
+	} );
+} );

+ 171 - 61
packages/ckeditor5-table/tests/converters/upcasttable.js

@@ -6,47 +6,27 @@
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
+import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 
-import upcastTable from '../../src/converters/upcasttable';
+import { defaultConversion, defaultSchema, formatTable, modelTable } from '../_utils/utils';
 
 describe( 'upcastTable()', () => {
 	let editor, model;
 
 	beforeEach( () => {
-		return VirtualTestEditor.create()
+		return VirtualTestEditor
+			.create( {
+				plugins: [ Paragraph, ImageEditing, Widget ]
+			} )
 			.then( newEditor => {
 				editor = newEditor;
 				model = editor.model;
 
-				const conversion = editor.conversion;
-				const schema = model.schema;
+				defaultSchema( model.schema, false );
 
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows', 'headingColumns' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
-
-				conversion.for( 'upcast' ).add( upcastTable() );
-
-				// Table row upcast only since downcast conversion is done in `downcastTable()`.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
-
-				// Table cell conversion.
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+				defaultConversion( editor.conversion, true );
 
 				// Since this part of test tests only view->model conversion editing pipeline is not necessary
 				// so defining model->view converters won't be necessary.
@@ -55,7 +35,7 @@ describe( 'upcastTable()', () => {
 	} );
 
 	function expectModel( data ) {
-		expect( getModelData( model, { withoutSelection: true } ) ).to.equal( data );
+		expect( formatTable( getModelData( model, { withoutSelection: true } ) ) ).to.equal( formatTable( data ) );
 	}
 
 	it( 'should convert table figure', () => {
@@ -69,7 +49,7 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table>' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
@@ -83,7 +63,7 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table>' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
@@ -91,7 +71,7 @@ describe( 'upcastTable()', () => {
 	it( 'should not convert empty figure', () => {
 		'<figure class="table"></figure>';
 
-		expectModel( '' );
+		expectModel( '<paragraph></paragraph>' );
 	} );
 
 	it( 'should convert if figure do not have class="table" attribute', () => {
@@ -105,7 +85,7 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table>' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
@@ -119,12 +99,12 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table headingRows="1">' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
 
-	it( 'should create table model from table with one thead with more then on row', () => {
+	it( 'should create table model from table with one thead with more then one row', () => {
 		editor.setData(
 			'<table>' +
 			'<thead>' +
@@ -137,9 +117,9 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table headingRows="3">' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
-			'<tableRow><tableCell>2</tableCell></tableRow>' +
-			'<tableRow><tableCell>3</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>2</paragraph></tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>3</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
@@ -155,9 +135,9 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table headingRows="1">' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
-			'<tableRow><tableCell>2</tableCell></tableRow>' +
-			'<tableRow><tableCell>3</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>2</paragraph></tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>3</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
@@ -172,8 +152,8 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table headingRows="1">' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
-			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>2</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
@@ -187,7 +167,7 @@ describe( 'upcastTable()', () => {
 
 		expectModel(
 			'<table>' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
 			'</table>'
 		);
 	} );
@@ -199,7 +179,7 @@ describe( 'upcastTable()', () => {
 		);
 
 		expectModel(
-			'<table><tableRow><tableCell></tableCell></tableRow></table>'
+			'<table><tableRow><tableCell><paragraph></paragraph></tableCell></tableRow></table>'
 		);
 	} );
 
@@ -212,17 +192,17 @@ describe( 'upcastTable()', () => {
 		);
 
 		expectModel(
-			'<table><tableRow><tableCell>bar</tableCell></tableRow></table>'
+			'<table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
 		);
 	} );
 
 	it( 'should create table model from some broken table', () => {
 		editor.setData(
-			'<table><td><p>foo</p></td></table>'
+			'<table><td><z>foo</z></td></table>'
 		);
 
 		expectModel(
-			'<table><tableRow><tableCell>foo</tableCell></tableRow></table>'
+			'<table><tableRow><tableCell><paragraph>foo</paragraph></tableCell></tableRow></table>'
 		);
 	} );
 
@@ -245,8 +225,8 @@ describe( 'upcastTable()', () => {
 		expectModel(
 			'<div>foo</div>' +
 			'<table headingRows="1">' +
-			'<tableRow><tableCell>1</tableCell></tableRow>' +
-			'<tableRow><tableCell>2</tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>1</paragraph></tableCell></tableRow>' +
+			'<tableRow><tableCell><paragraph>2</paragraph></tableCell></tableRow>' +
 			'</table>' +
 			'<div>bar</div>'
 		);
@@ -258,8 +238,19 @@ describe( 'upcastTable()', () => {
 			allowAttributes: [ 'headingRows' ],
 			isObject: true
 		} );
+		editor.model.schema.register( 'fooCell', {
+			allowIn: 'fooRow',
+			isObject: true
+		} );
+		editor.model.schema.register( 'fooRow', {
+			allowIn: 'fooTable',
+			isObject: true
+		} );
 
 		editor.conversion.elementToElement( { model: 'fooTable', view: 'table', converterPriority: 'high' } );
+		editor.conversion.elementToElement( { model: 'fooRow', view: 'tr', converterPriority: 'high' } );
+		editor.conversion.elementToElement( { model: 'fooCell', view: 'td', converterPriority: 'high' } );
+		editor.conversion.elementToElement( { model: 'fooCell', view: 'th', converterPriority: 'high' } );
 
 		editor.setData(
 			'<table>' +
@@ -268,7 +259,33 @@ describe( 'upcastTable()', () => {
 		);
 
 		expectModel(
-			'<fooTable></fooTable>'
+			'<fooTable><fooRow><fooCell></fooCell></fooRow></fooTable>'
+		);
+	} );
+
+	it( 'should strip table in table', () => {
+		editor.setData(
+			'<table>' +
+				'<tr>' +
+					'<td>' +
+						'<table>' +
+							'<tr>' +
+								'<td>tableception</td>' +
+							'</tr>' +
+						'</table>' +
+					'</td>' +
+				'</tr>' +
+			'</table>'
+		);
+
+		expectModel(
+			'<table>' +
+				'<tableRow>' +
+					'<tableCell>' +
+						'<paragraph>tableception</paragraph>' +
+					'</tableCell>' +
+				'</tableRow>' +
+			'</table>'
 		);
 	} );
 
@@ -296,19 +313,34 @@ describe( 'upcastTable()', () => {
 			expectModel(
 				'<table headingColumns="2" headingRows="1">' +
 				'<tableRow>' +
-				'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
+					'<tableCell><paragraph>11</paragraph></tableCell>' +
+					'<tableCell><paragraph>12</paragraph></tableCell>' +
+					'<tableCell><paragraph>13</paragraph></tableCell>' +
+					'<tableCell><paragraph>14</paragraph></tableCell>' +
 				'</tableRow>' +
 				'<tableRow>' +
-				'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
+					'<tableCell><paragraph>21</paragraph></tableCell>' +
+					'<tableCell><paragraph>22</paragraph></tableCell>' +
+					'<tableCell><paragraph>23</paragraph></tableCell>' +
+					'<tableCell><paragraph>24</paragraph></tableCell>' +
 				'</tableRow>' +
 				'<tableRow>' +
-				'<tableCell>31</tableCell><tableCell>32</tableCell><tableCell>33</tableCell><tableCell>34</tableCell>' +
+					'<tableCell><paragraph>31</paragraph></tableCell>' +
+					'<tableCell><paragraph>32</paragraph></tableCell>' +
+					'<tableCell><paragraph>33</paragraph></tableCell>' +
+					'<tableCell><paragraph>34</paragraph></tableCell>' +
 				'</tableRow>' +
 				'<tableRow>' +
-				'<tableCell>41</tableCell><tableCell>42</tableCell><tableCell>43</tableCell><tableCell>44</tableCell>' +
+					'<tableCell><paragraph>41</paragraph></tableCell>' +
+					'<tableCell><paragraph>42</paragraph></tableCell>' +
+					'<tableCell><paragraph>43</paragraph></tableCell>' +
+					'<tableCell><paragraph>44</paragraph></tableCell>' +
 				'</tableRow>' +
 				'<tableRow>' +
-				'<tableCell>51</tableCell><tableCell>52</tableCell><tableCell>53</tableCell><tableCell>54</tableCell>' +
+					'<tableCell><paragraph>51</paragraph></tableCell>' +
+					'<tableCell><paragraph>52</paragraph></tableCell>' +
+					'<tableCell><paragraph>53</paragraph></tableCell>' +
+					'<tableCell><paragraph>54</paragraph></tableCell>' +
 				'</tableRow>' +
 				'</table>'
 			);
@@ -332,16 +364,94 @@ describe( 'upcastTable()', () => {
 			expectModel(
 				'<table headingColumns="3" headingRows="1">' +
 				'<tableRow>' +
-				'<tableCell>11</tableCell><tableCell>12</tableCell><tableCell>13</tableCell><tableCell>14</tableCell>' +
+					'<tableCell><paragraph>11</paragraph></tableCell>' +
+					'<tableCell><paragraph>12</paragraph></tableCell>' +
+					'<tableCell><paragraph>13</paragraph></tableCell>' +
+					'<tableCell><paragraph>14</paragraph></tableCell>' +
 				'</tableRow>' +
 				'<tableRow>' +
-				'<tableCell>21</tableCell><tableCell>22</tableCell><tableCell>23</tableCell><tableCell>24</tableCell>' +
+					'<tableCell><paragraph>21</paragraph></tableCell>' +
+					'<tableCell><paragraph>22</paragraph></tableCell>' +
+					'<tableCell><paragraph>23</paragraph></tableCell>' +
+					'<tableCell><paragraph>24</paragraph></tableCell>' +
 				'</tableRow>' +
 				'<tableRow>' +
-				'<tableCell colspan="2">31</tableCell><tableCell>33</tableCell><tableCell>34</tableCell>' +
+				'<tableCell colspan="2"><paragraph>31</paragraph></tableCell>' +
+					'<tableCell><paragraph>33</paragraph></tableCell>' +
+					'<tableCell><paragraph>34</paragraph></tableCell>' +
 				'</tableRow>' +
 				'</table>'
 			);
 		} );
 	} );
+
+	describe( 'block contents', () => {
+		it( 'should upcast table with empty table cell to paragraph', () => {
+			editor.setData(
+				'<table>' +
+					'<tbody>' +
+						'<tr>' +
+							'<td>foo</td>' +
+						'</tr>' +
+					'</tbody>' +
+				'</table>'
+			);
+
+			expectModel( modelTable( [
+				[ 'foo' ]
+			] ) );
+		} );
+
+		it( 'should upcast table with <p> in table cell', () => {
+			editor.setData(
+				'<table>' +
+					'<tbody>' +
+						'<tr>' +
+							'<td><p>foo</p></td>' +
+						'</tr>' +
+					'</tbody>' +
+				'</table>'
+			);
+
+			expectModel( modelTable( [
+				[ 'foo' ]
+			] ) );
+		} );
+
+		it( 'should upcast table with multiple <p> in table cell', () => {
+			editor.setData(
+				'<table>' +
+					'<tbody>' +
+						'<tr>' +
+							'<td>' +
+								'<p>foo</p>' +
+								'<p>bar</p>' +
+								'<p>baz</p>' +
+							'</td>' +
+						'</tr>' +
+					'</tbody>' +
+				'</table>'
+			);
+
+			expectModel( modelTable( [
+				[ '<paragraph>foo</paragraph><paragraph>bar</paragraph><paragraph>baz</paragraph>' ]
+			] ) );
+		} );
+
+		it( 'should upcast table with <img> in table cell to empty table cell', () => {
+			editor.setData(
+				'<table>' +
+					'<tbody>' +
+						'<tr>' +
+							'<td><img src="sample.png"></td>' +
+						'</tr>' +
+					'</tbody>' +
+				'</table>'
+			);
+
+			expectModel( modelTable( [
+				[ '' ]
+			] ) );
+		} );
+	} );
 } );

+ 12 - 3
packages/ckeditor5-table/tests/integration.js

@@ -55,7 +55,10 @@ describe( 'TableToolbar integration', () => {
 		} );
 
 		it( 'should allow the BalloonToolbar to be displayed when a table content is selected', () => {
-			setModelData( newEditor.model, '<paragraph>foo</paragraph><table><tableRow><tableCell>x[y]z</tableCell></tableRow></table>' );
+			setModelData(
+				newEditor.model,
+				'<paragraph>foo</paragraph><table><tableRow><tableCell><paragraph>x[y]z</paragraph></tableCell></tableRow></table>'
+			);
 
 			balloonToolbar.show();
 
@@ -63,7 +66,10 @@ describe( 'TableToolbar integration', () => {
 		} );
 
 		it( 'should prevent the BalloonToolbar from being displayed when a table is selected as whole', () => {
-			setModelData( newEditor.model, '<paragraph>foo</paragraph>[<table><tableRow><tableCell>foo</tableCell></tableRow></table>]' );
+			setModelData(
+				newEditor.model,
+				'<paragraph>foo</paragraph>[<table><tableRow><tableCell><paragraph>foo</paragraph></tableCell></tableRow></table>]'
+			);
 
 			balloonToolbar.show();
 
@@ -76,7 +82,10 @@ describe( 'TableToolbar integration', () => {
 			const normalPrioritySpy = sinon.spy();
 
 			// Select an table
-			setModelData( newEditor.model, '<paragraph>foo</paragraph>[<table><tableRow><tableCell>x</tableCell></tableRow></table>]' );
+			setModelData(
+				newEditor.model,
+				'<paragraph>foo</paragraph>[<table><tableRow><tableCell><paragraph>x</paragraph></tableCell></tableRow></table>]'
+			);
 
 			newEditor.listenTo( balloonToolbar, 'show', highestPrioritySpy, { priority: 'highest' } );
 			newEditor.listenTo( balloonToolbar, 'show', highPrioritySpy, { priority: 'high' } );

BIN
packages/ckeditor5-table/tests/manual/sample.jpg


+ 4 - 2
packages/ckeditor5-table/tests/manual/table.html

@@ -6,7 +6,7 @@
 </style>
 
 <div id="editor">
-	<p>Table with everything:</p>
+	<p>Complex table:</p>
 
 	<figure>
 		<figcaption>Data about the planets of our solar system (Planetary facts taken from <a
@@ -140,7 +140,9 @@
 				<td>5906.4</td>
 				<td>-225</td>
 				<td>5</td>
-				<td>Declassified as a planet in 2006, but this <a href="http://www.usatoday.com/story/tech/2014/10/02/pluto-planet-solar-system/16578959/">remains controversial</a>.</td>
+				<td>Declassified as a planet in 2006, but this <a
+					href="http://www.usatoday.com/story/tech/2014/10/02/pluto-planet-solar-system/16578959/">remains controversial</a>.
+				</td>
 			</tr>
 			</tbody>
 		</table>

+ 69 - 0
packages/ckeditor5-table/tests/manual/tableblockcontent.html

@@ -0,0 +1,69 @@
+<style>
+	body {
+		font-family: Helvetica, Arial, sans-serif;
+		font-size: 14px;
+	}
+</style>
+
+<div id="editor">
+
+	<p>Table with block contents:</p>
+
+	<table>
+		<tbody>
+		<tr>
+			<td>$text</td>
+			<td>Foo</td>
+			<td>Brownie carrot cake soufflé jelly-o lemon drops cupcake tart.</td>
+		</tr>
+		<tr>
+			<td>paragraph</td>
+			<td><p>Foo</p></td>
+			<td>
+				<p>A paragraph with default alignment</p>
+				<p style="text-align: right">A paragraph with right alignment</p>
+				<p style="text-align: center">A paragraph with center alignment</p>
+				<p style="text-align: justify">A paragraph with justify alignment</p>
+			</td>
+		</tr>
+		<tr>
+			<td>list</td>
+			<td>
+				<ul>
+					<li>Foo</li>
+					<li>Bar</li>
+				</ul>
+			</td>
+			<td>
+				<ul>
+					<li>A list item with default alignment</li>
+					<li style="text-align: right">A list item with right alignment</li>
+					<li style="text-align: center">A list item with center alignment</li>
+					<li style="text-align: justify">A list item with justify alignment</li>
+				</ul>
+			</td>
+		</tr>
+		<tr>
+			<td>heading</td>
+			<td>
+				<h2>An h2 title.</h2>
+				<h3>An h3 title.</h3>
+				<h4>An h4 title.</h4>
+			</td>
+			<td>
+				<h2>An h2 with default alignment.</h2>
+				<h2 style="text-align: right">An h2 with right alignment.</h2>
+				<h2 style="text-align: center">An h2 with center alignment.</h2>
+				<h2 style="text-align: justify">An h2 with justify alignment.</h2>
+			</td>
+		</tr>
+		<tr>
+			<td>block quote</td>
+			<td>
+				<blockquote><p>A quote</p></blockquote>
+			</td>
+			<td><blockquote><p style="text-align: right">A quote with paragraph with right alignment</p></blockquote></td>
+		</tr>
+		</tbody>
+	</table>
+</div>

+ 30 - 0
packages/ckeditor5-table/tests/manual/tableblockcontent.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Table from '../../src/table';
+import TableToolbar from '../../src/tabletoolbar';
+import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet, Table, TableToolbar, Alignment ],
+		toolbar: [
+			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote',
+			'alignment', '|', 'undo', 'redo'
+		],
+		table: {
+			toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 19 - 0
packages/ckeditor5-table/tests/manual/tableblockcontent.md

@@ -0,0 +1,19 @@
+### Loading
+
+1. The data should be loaded with a table with block contents in second column:
+
+    * Paragraph - it should be rendered as `<span>` if single paragraph is inside table cell.
+    * List
+    * Heading
+    * Block Quote (with inner paragraph)
+    * Image
+
+2. The third column consist blocks with text alignment.
+    * Paragraph - should be rendered was `<p>` when alignment is set (apart from default) for single paragraph.
+
+### Testing
+
+1. Use <kbd>Enter</kbd> in cells with single `<pargraph>`. When two `<paragraph>`'s are in one table cell they should be rendered as `<p>`.
+2. Undo previous step - the `<p>` element should be changed to `<span>` for single paragraph.
+3. Change `<heading>` to paragraph - it should be rendered as `<p>` element if there are other headings or other block content.
+4. Change one `<heading>` to paragraph and remove other headings. The `<paragraph>` should be rendered as `<span>`. 

+ 151 - 0
packages/ckeditor5-table/tests/table-integration.js

@@ -0,0 +1,151 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Widget from '@ckeditor/ckeditor5-widget/src/widget';
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
+import ListEditing from '@ckeditor/ckeditor5-list/src/listediting';
+import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import {
+	getData as getModelData,
+	setData as setModelData
+} from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+
+import TableEditing from '../src/tableediting';
+import { formatTable, formattedModelTable, modelTable, viewTable } from './_utils/utils';
+
+describe( 'Table feature – integration', () => {
+	describe( 'with clipboard', () => {
+		let editor, clipboard;
+
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } )
+				.then( newEditor => {
+					editor = newEditor;
+					clipboard = editor.plugins.get( 'Clipboard' );
+				} );
+		} );
+
+		it( 'pastes td as p when pasting into the table', () => {
+			setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) );
+
+			clipboard.fire( 'inputTransformation', {
+				content: parseView( '<td>bar</td>' )
+			} );
+
+			expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [
+				[ 'foobar[]' ]
+			] ) );
+		} );
+
+		it( 'pastes td as p when pasting into the p', () => {
+			setModelData( editor.model, '<paragraph>foo[]</paragraph>' );
+
+			clipboard.fire( 'inputTransformation', {
+				content: parseView( '<td>bar</td>' )
+			} );
+
+			expect( formatTable( getModelData( editor.model ) ) ).to.equal( '<paragraph>foobar[]</paragraph>' );
+		} );
+
+		it( 'pastes list into the td', () => {
+			setModelData( editor.model, modelTable( [ [ '[]' ] ] ) );
+
+			clipboard.fire( 'inputTransformation', {
+				content: parseView( '<li>bar</li>' )
+			} );
+
+			expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [
+				[ '<listItem listIndent="0" listType="bulleted">bar[]</listItem>' ]
+			] ) );
+		} );
+
+		it( 'pastes blockquote into the td', () => {
+			setModelData( editor.model, modelTable( [ [ '[]' ] ] ) );
+
+			clipboard.fire( 'inputTransformation', {
+				content: parseView( '<blockquote>bar</blockquote>' )
+			} );
+
+			expect( formatTable( getModelData( editor.model ) ) ).to.equal( formattedModelTable( [
+				[ '<blockQuote><paragraph>bar[]</paragraph></blockQuote>' ]
+			] ) );
+		} );
+	} );
+
+	describe( 'with undo', () => {
+		let editor, doc, root;
+
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } )
+				.then( newEditor => {
+					editor = newEditor;
+					doc = editor.model.document;
+					root = doc.getRoot();
+				} );
+		} );
+
+		it( 'fixing empty roots should be transparent to undo', () => {
+			expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
+			expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
+
+			editor.data.set( viewTable( [ [ 'foo' ] ] ) );
+
+			expect( editor.getData() ).to.equal( viewTable( [ [ 'foo' ] ] ) );
+
+			editor.model.change( writer => {
+				writer.remove( root.getChild( 0 ) );
+			} );
+
+			expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
+
+			editor.execute( 'undo' );
+
+			expect( editor.getData() ).to.equal( viewTable( [ [ 'foo' ] ] ) );
+
+			editor.execute( 'redo' );
+
+			expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
+
+			editor.execute( 'undo' );
+
+			expect( editor.getData() ).to.equal( viewTable( [ [ 'foo' ] ] ) );
+		} );
+
+		it( 'fixing empty roots should be transparent to undo - multiple roots', () => {
+			const otherRoot = doc.createRoot( '$root', 'otherRoot' );
+
+			editor.data.set( viewTable( [ [ 'foo' ] ] ), 'main' );
+			editor.data.set( viewTable( [ [ 'foo' ] ] ), 'otherRoot' );
+
+			editor.model.change( writer => {
+				writer.remove( root.getChild( 0 ) );
+			} );
+
+			editor.model.change( writer => {
+				writer.remove( otherRoot.getChild( 0 ) );
+			} );
+
+			expect( editor.data.get( 'main' ) ).to.equal( '<p>&nbsp;</p>' );
+			expect( editor.data.get( 'otherRoot' ) ).to.equal( '<p>&nbsp;</p>' );
+
+			editor.execute( 'undo' );
+
+			expect( editor.data.get( 'main' ) ).to.equal( '<p>&nbsp;</p>' );
+			expect( editor.data.get( 'otherRoot' ) ).to.equal( viewTable( [ [ 'foo' ] ] ) );
+
+			editor.execute( 'undo' );
+
+			expect( editor.data.get( 'main' ) ).to.equal( viewTable( [ [ 'foo' ] ] ) );
+			expect( editor.data.get( 'otherRoot' ) ).to.equal( viewTable( [ [ 'foo' ] ] ) );
+		} );
+	} );
+} );

+ 174 - 5
packages/ckeditor5-table/tests/tableediting.js

@@ -7,6 +7,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
 
 import TableEditing from '../src/tableediting';
 import { formatTable, formattedModelTable, modelTable } from './_utils/utils';
@@ -26,7 +27,7 @@ describe( 'TableEditing', () => {
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ TableEditing, Paragraph ]
+				plugins: [ TableEditing, Paragraph, ImageEditing ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -40,6 +41,39 @@ describe( 'TableEditing', () => {
 	} );
 
 	it( 'should set proper schema rules', () => {
+		// Table:
+		expect( model.schema.isRegistered( 'table' ) ).to.be.true;
+		expect( model.schema.isObject( 'table' ) ).to.be.true;
+		expect( model.schema.isLimit( 'table' ) ).to.be.true;
+
+		expect( model.schema.checkChild( [ '$root' ], 'table' ) ).to.be.true;
+		expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingRows' ) ).to.be.true;
+		expect( model.schema.checkAttribute( [ '$root', 'table' ], 'headingColumns' ) ).to.be.true;
+
+		// Table row:
+		expect( model.schema.isRegistered( 'tableRow' ) ).to.be.true;
+		expect( model.schema.isLimit( 'tableRow' ) ).to.be.true;
+
+		expect( model.schema.checkChild( [ '$root' ], 'tableRow' ) ).to.be.false;
+		expect( model.schema.checkChild( [ 'table' ], 'tableRow' ) ).to.be.true;
+
+		// Table cell:
+		expect( model.schema.isRegistered( 'tableCell' ) ).to.be.true;
+		expect( model.schema.isLimit( 'tableCell' ) ).to.be.true;
+
+		expect( model.schema.checkChild( [ '$root' ], 'tableCell' ) ).to.be.false;
+		expect( model.schema.checkChild( [ 'table' ], 'tableCell' ) ).to.be.false;
+		expect( model.schema.checkChild( [ 'tableRow' ], 'tableCell' ) ).to.be.true;
+		expect( model.schema.checkChild( [ 'tableCell' ], 'tableCell' ) ).to.be.false;
+
+		expect( model.schema.checkAttribute( [ 'tableCell' ], 'colspan' ) ).to.be.true;
+		expect( model.schema.checkAttribute( [ 'tableCell' ], 'rowspan' ) ).to.be.true;
+
+		// Table cell contents:
+		expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$text' ) ).to.be.false;
+		expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], '$block' ) ).to.be.true;
+		expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'table' ) ).to.be.false;
+		expect( model.schema.checkChild( [ '$root', 'table', 'tableRow', 'tableCell' ], 'image' ) ).to.be.false;
 	} );
 
 	it( 'adds insertTable command', () => {
@@ -105,7 +139,7 @@ describe( 'TableEditing', () => {
 	describe( 'conversion in data pipeline', () => {
 		describe( 'model to view', () => {
 			it( 'should create tbody section', () => {
-				setModelData( model, '<table><tableRow><tableCell>foo[]</tableCell></tableRow></table>' );
+				setModelData( model, '<table><tableRow><tableCell><paragraph>foo[]</paragraph></tableCell></tableRow></table>' );
 
 				expect( editor.getData() ).to.equal(
 					'<figure class="table">' +
@@ -119,7 +153,10 @@ describe( 'TableEditing', () => {
 			} );
 
 			it( 'should create thead section', () => {
-				setModelData( model, '<table headingRows="1"><tableRow><tableCell>foo[]</tableCell></tableRow></table>' );
+				setModelData(
+					model,
+					'<table headingRows="1"><tableRow><tableCell><paragraph>foo[]</paragraph></tableCell></tableRow></table>'
+				);
 
 				expect( editor.getData() ).to.equal(
 					'<figure class="table">' +
@@ -138,7 +175,14 @@ describe( 'TableEditing', () => {
 				editor.setData( '<table><tbody><tr><td>foo</td></tr></tbody></table>' );
 
 				expect( getModelData( model, { withoutSelection: true } ) )
-					.to.equal( '<table><tableRow><tableCell>foo</tableCell></tableRow></table>' );
+					.to.equal( '<table><tableRow><tableCell><paragraph>foo</paragraph></tableCell></tableRow></table>' );
+			} );
+
+			it( 'should convert table with image', () => {
+				editor.setData( '<table><tbody><tr><td><img src="sample.png"></td></tr></tbody></table>' );
+
+				expect( getModelData( model, { withoutSelection: true } ) )
+					.to.equal( '<table><tableRow><tableCell><paragraph></paragraph></tableCell></tableRow></table>' );
 			} );
 		} );
 	} );
@@ -242,6 +286,40 @@ describe( 'TableEditing', () => {
 				] ) );
 			} );
 
+			it( 'should move to the next table cell if part of block content is selected', () => {
+				setModelData( model, modelTable( [
+					[ '11', '<paragraph>12</paragraph><paragraph>[foo]</paragraph><paragraph>bar</paragraph>', '13' ],
+				] ) );
+
+				editor.editing.view.document.fire( 'keydown', domEvtDataStub );
+
+				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
+					[
+						'11',
+						'<paragraph>12</paragraph><paragraph>foo</paragraph><paragraph>bar</paragraph>',
+						'[13]'
+					],
+				] ) );
+			} );
+
+			it( 'should listen with lower priority then its children', () => {
+				// Cancel TAB event.
+				editor.keystrokes.set( 'Tab', ( data, cancel ) => cancel() );
+
+				setModelData( model, modelTable( [
+					[ '11[]', '12' ]
+				] ) );
+
+				editor.editing.view.document.fire( 'keydown', domEvtDataStub );
+
+				sinon.assert.calledOnce( domEvtDataStub.preventDefault );
+				sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
+
+				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
+					[ '11[]', '12' ]
+				] ) );
+			} );
+
 			describe( 'on table widget selected', () => {
 				beforeEach( () => {
 					editor.model.schema.register( 'block', {
@@ -256,7 +334,7 @@ describe( 'TableEditing', () => {
 				it( 'should move caret to the first table cell on TAB', () => {
 					const spy = sinon.spy();
 
-					editor.editing.view.document.on( 'keydown', spy );
+					editor.keystrokes.set( 'Tab', spy, { priority: 'lowest' } );
 
 					setModelData( model, '[' + modelTable( [
 						[ '11', '12' ]
@@ -326,6 +404,7 @@ describe( 'TableEditing', () => {
 
 				sinon.assert.calledOnce( domEvtDataStub.preventDefault );
 				sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
+
 				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
 					[ '[11]', '12' ]
 				] ) );
@@ -356,6 +435,96 @@ describe( 'TableEditing', () => {
 					[ '21', '22' ]
 				] ) );
 			} );
+
+			it( 'should move to the previous table cell if part of block content is selected', () => {
+				setModelData( model, modelTable( [
+					[ '11', '<paragraph>12</paragraph><paragraph>[foo]</paragraph><paragraph>bar</paragraph>', '13' ],
+				] ) );
+
+				editor.editing.view.document.fire( 'keydown', domEvtDataStub );
+
+				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
+					[
+						'[11]',
+						'<paragraph>12</paragraph><paragraph>foo</paragraph><paragraph>bar</paragraph>',
+						'13'
+					],
+				] ) );
+			} );
+		} );
+	} );
+
+	describe( 'enter key', () => {
+		let evtDataStub, viewDocument;
+
+		beforeEach( () => {
+			evtDataStub = {
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy(),
+				isSoft: false
+			};
+
+			return VirtualTestEditor
+				.create( {
+					plugins: [ TableEditing, Paragraph ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					sinon.stub( editor, 'execute' );
+
+					viewDocument = editor.editing.view.document;
+					model = editor.model;
+				} );
+		} );
+
+		it( 'should do nothing if not in table cell', () => {
+			setModelData( model, '<paragraph>[]foo</paragraph>' );
+
+			viewDocument.fire( 'enter', evtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+			expect( formatTable( getModelData( model ) ) ).to.equal( '<paragraph>[]foo</paragraph>' );
+		} );
+
+		it( 'should do nothing if table cell has already a block content', () => {
+			setModelData( model, modelTable( [
+				[ '<paragraph>[]11</paragraph>' ]
+			] ) );
+
+			viewDocument.fire( 'enter', evtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+			expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
+				[ '<paragraph>[]11</paragraph>' ]
+			] ) );
+		} );
+
+		it( 'should do nothing if table cell with a block content is selected as a whole', () => {
+			setModelData( model, modelTable( [
+				[ '<paragraph>[1</paragraph><paragraph>1]</paragraph>' ]
+			] ) );
+
+			viewDocument.fire( 'enter', evtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+			setModelData( model, modelTable( [
+				[ '<paragraph>[1</paragraph><paragraph>1]</paragraph>' ]
+			] ) );
+		} );
+
+		it( 'should allow default behavior of Shift+Enter pressed', () => {
+			setModelData( model, modelTable( [
+				[ '[]11' ]
+			] ) );
+
+			evtDataStub.isSoft = true;
+			viewDocument.fire( 'enter', evtDataStub );
+
+			sinon.assert.notCalled( editor.execute );
+			expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
+				[ '[]11' ]
+			] ) );
 		} );
 	} );
 } );

+ 9 - 3
packages/ckeditor5-table/tests/tabletoolbar.js

@@ -123,7 +123,10 @@ describe( 'TableToolbar', () => {
 		} );
 
 		it( 'should show the toolbar on ui#update when the table content is selected', () => {
-			setData( model, '<paragraph>[foo]</paragraph><table><tableRow><tableCell>bar</tableCell></tableRow></table>' );
+			setData(
+				model,
+				'<paragraph>[foo]</paragraph><table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+			);
 
 			expect( balloon.visibleView ).to.be.null;
 
@@ -147,7 +150,7 @@ describe( 'TableToolbar', () => {
 		} );
 
 		it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
-			setData( model, '<table><tableRow><tableCell>x[y]z</tableCell></tableRow></table>' );
+			setData( model, '<table><tableRow><tableCell><paragraph>x[y]z</paragraph></tableCell></tableRow></table>' );
 
 			expect( balloon.visibleView ).to.equal( toolbar );
 
@@ -170,7 +173,10 @@ describe( 'TableToolbar', () => {
 		} );
 
 		it( 'should hide the toolbar on render if the table is de–selected', () => {
-			setData( model, '<paragraph>foo</paragraph><table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+			setData(
+				model,
+				'<paragraph>foo</paragraph><table><tableRow><tableCell><paragraph>[]</paragraph></tableCell></tableRow></table>'
+			);
 
 			expect( balloon.visibleView ).to.equal( toolbar );
 

+ 6 - 55
packages/ckeditor5-table/tests/tableutils.js

@@ -4,19 +4,10 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
-
-import {
-	downcastInsertCell,
-	downcastInsertRow,
-	downcastInsertTable,
-	downcastRemoveRow,
-	downcastTableHeadingColumnsChange,
-	downcastTableHeadingRowsChange
-} from '../src/converters/downcast';
-import upcastTable from '../src/converters/upcasttable';
-import { formatTable, formattedModelTable, modelTable } from './_utils/utils';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from './_utils/utils';
+
 import TableUtils from '../src/tableutils';
 
 describe( 'TableUtils', () => {
@@ -31,48 +22,8 @@ describe( 'TableUtils', () => {
 			root = model.document.getRoot( 'main' );
 			tableUtils = editor.plugins.get( TableUtils );
 
-			const conversion = editor.conversion;
-			const schema = model.schema;
-
-			schema.register( 'table', {
-				allowWhere: '$block',
-				allowAttributes: [ 'headingRows' ],
-				isObject: true
-			} );
-
-			schema.register( 'tableRow', { allowIn: 'table' } );
-
-			schema.register( 'tableCell', {
-				allowIn: 'tableRow',
-				allowContentOf: '$block',
-				allowAttributes: [ 'colspan', 'rowspan' ],
-				isLimit: true
-			} );
-
-			model.schema.register( 'p', { inheritAllFrom: '$block' } );
-
-			// Table conversion.
-			conversion.for( 'upcast' ).add( upcastTable() );
-			conversion.for( 'downcast' ).add( downcastInsertTable() );
-
-			// Insert row conversion.
-			conversion.for( 'downcast' ).add( downcastInsertRow() );
-
-			// Remove row conversion.
-			conversion.for( 'downcast' ).add( downcastRemoveRow() );
-
-			// Table cell conversion.
-			conversion.for( 'downcast' ).add( downcastInsertCell() );
-
-			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
-
-			// Table attributes conversion.
-			conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-			conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
-
-			conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-			conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+			defaultSchema( model.schema );
+			defaultConversion( editor.conversion );
 		} );
 	} );
 

+ 5 - 18
packages/ckeditor5-table/tests/tablewalker.js

@@ -5,7 +5,8 @@
 
 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 { defaultConversion, defaultSchema, modelTable } from './_utils/utils';
 
 import TableWalker from '../src/tablewalker';
 
@@ -20,22 +21,8 @@ describe( 'TableWalker', () => {
 				doc = model.document;
 				root = doc.getRoot( 'main' );
 
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows', 'headingColumns' ],
-					isObject: true
-				} );
-
-				schema.register( 'tableRow', { allowIn: 'table' } );
-
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
 			} );
 	} );
 
@@ -53,7 +40,7 @@ describe( 'TableWalker', () => {
 		const formattedResult = result.map( ( { row, column, cell, cellIndex } ) => ( {
 			row,
 			column,
-			data: cell && cell.getChild( 0 ).data,
+			data: cell && cell.getChild( 0 ).getChild( 0 ).data,
 			index: cellIndex
 		} ) );
 

+ 0 - 1
packages/ckeditor5-table/theme/table.css

@@ -21,7 +21,6 @@
 		& th {
 			min-width: 2em;
 			padding: .4em;
-			text-align: center;
 			border-color: hsl(0, 0%, 85%);
 		}