Explorar o código

Merge branch 'master' into i/7227

Aleksander Nowodzinski %!s(int64=5) %!d(string=hai) anos
pai
achega
582af860d8

+ 31 - 0
.github/workflows/merge-stable.yml

@@ -0,0 +1,31 @@
+name: Stable branch auto merge
+
+on:
+  push:
+    branches: [ stable ]
+
+jobs:
+  merge:
+    runs-on: ubuntu-latest
+    steps:
+      # First: merge
+      - uses: octokit/request-action@v2.x
+        id: merge_action
+        with:
+          route: POST /repos/:repository/merges
+          repository: ${{ github.repository }}
+          base: master
+          head: stable
+        env:
+          GITHUB_TOKEN: ${{ secrets.STABLE_MERGE_GITHUB_TOKEN }}
+      # Report errors if any
+      - uses: rtCamp/action-slack-notify@v2.0.2
+        id: error_message_slack
+        name: Slack notification
+        if: (steps.merge_action.outputs.status != 201) && (steps.merge_action.outputs.status != 204)
+        env:
+          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+          SLACK_CHANNEL: "cke5-ci"
+          SLACK_ICON: https://github.com/ckeditor.png?size=48
+          SLACK_USERNAME: "stable auto merge action"
+          SLACK_MESSAGE: ${{ format('💥 Error, merge call returned code {0}, see {1} for a list of codes with explanation. 💥', steps.merge_action.outputs.status, 'https://developer.github.com/v3/repos/merging/#perform-a-merge' ) }}

+ 33 - 27
packages/ckeditor5-engine/tests/view/view/view.js

@@ -17,6 +17,7 @@ import CompositionObserver from '../../../src/view/observer/compositionobserver'
 import ViewRange from '../../../src/view/range';
 import ViewElement from '../../../src/view/element';
 import ViewContainerElement from '../../../src/view/containerelement';
+import ViewText from '../../../src/view/text';
 import ViewPosition from '../../../src/view/position';
 import ViewSelection from '../../../src/view/selection';
 import { StylesProcessor } from '../../../src/view/stylesmap';
@@ -516,7 +517,7 @@ describe( 'view', () => {
 	} );
 
 	describe( 'hasDomSelection', () => {
-		let domElement, domP, domSelection;
+		let domElement, domP, domText, viewP, viewText, domSelection;
 
 		// Focus tests are too unstable on Firefox to run them.
 		if ( env.isGecko ) {
@@ -528,10 +529,12 @@ describe( 'view', () => {
 
 			view.attachDomRoot( domRoot );
 
-			// It must be a container element to be rendered with the bogus <br> inside which ensures
-			// that the browser sees a selection position inside (empty <p> may not be selectable).
-			// May help resolving https://github.com/ckeditor/ckeditor5/issues/6655.
-			viewRoot._appendChild( new ViewContainerElement( viewDocument, 'p' ) );
+			viewP = new ViewContainerElement( viewDocument, 'p' );
+			viewText = new ViewText( viewDocument, 'ab' );
+
+			viewRoot._appendChild( viewP );
+			viewP._appendChild( viewText );
+
 			view.forceRender();
 
 			domElement = createElement( document, 'div', { contenteditable: 'true' } );
@@ -539,6 +542,7 @@ describe( 'view', () => {
 
 			domSelection = document.getSelection();
 			domP = domRoot.childNodes[ 0 ];
+			domText = domP.childNodes[ 0 ];
 		} );
 
 		afterEach( () => {
@@ -548,38 +552,40 @@ describe( 'view', () => {
 		it( 'should be true if selection is inside a DOM root element', done => {
 			domRoot.focus();
 
-			setTimeout( () => {
-				domSelection.collapse( domP, 0 );
+			// Both selection need to stay in sync to avoid inf selection loops
+			// as there's no editing pipeline that would ensure that the view selection
+			// gets changed based on the selectionChange event. See https://github.com/ckeditor/ckeditor5/issues/6655.
+			viewDocument.selection._setTo( viewText, 1 );
+			domSelection.collapse( domText, 1 );
 
-				// Wait for async selectionchange event on DOM document.
-				setTimeout( () => {
-					expect( view.hasDomSelection ).to.be.true;
+			// Wait for async selectionchange event on DOM document.
+			setTimeout( () => {
+				expect( view.hasDomSelection ).to.be.true;
 
-					done();
-				}, 10 );
-			}, 10 );
+				done();
+			}, 1000 );
 		} );
 
 		it( 'should be true if selection is inside a DOM root element - no focus', done => {
 			domRoot.focus();
 
+			// See the previous test.
+			viewDocument.selection._setTo( viewText, 1 );
+			domSelection.collapse( domText, 1 );
+
 			setTimeout( () => {
-				domSelection.collapse( domP, 0 );
+				// We could also do domRoot.blur() here but it's always better to know where the focus went.
+				// E.g. if it went to some <input>, the selection would disappear from the editor and the test would fail.
+				domRoot.blur();
 
+				// Wait for async selectionchange event on DOM document.
 				setTimeout( () => {
-					// We could also do domRoot.blur() here but it's always better to know where the focus went.
-					// E.g. if it went to some <input>, the selection would disappear from the editor and the test would fail.
-					domRoot.blur();
-
-					// Wait for async selectionchange event on DOM document.
-					setTimeout( () => {
-						expect( view.hasDomSelection ).to.be.true;
-						expect( view.document.isFocused ).to.be.false;
-
-						done();
-					}, 10 );
-				}, 10 );
-			}, 10 );
+					expect( view.hasDomSelection ).to.be.true;
+					expect( view.document.isFocused ).to.be.false;
+
+					done();
+				}, 100 );
+			}, 100 );
 		} );
 
 		it( 'should be false if selection is outside DOM root element', done => {

+ 2 - 1
packages/ckeditor5-media-embed/package.json

@@ -26,7 +26,8 @@
     "@ckeditor/ckeditor5-link": "^19.0.0",
     "@ckeditor/ckeditor5-list": "^19.0.0",
     "@ckeditor/ckeditor5-paragraph": "^19.0.0",
-    "@ckeditor/ckeditor5-typing": "^19.0.0"
+    "@ckeditor/ckeditor5-typing": "^19.0.0",
+    "@ckeditor/ckeditor5-table": "^19.0.0"
   },
   "engines": {
     "node": ">=8.0.0",

+ 7 - 0
packages/ckeditor5-media-embed/src/automediaembed.js

@@ -129,11 +129,15 @@ export default class AutoMediaEmbed extends Plugin {
 
 		// If the URL does not match to universal URL regexp, let's skip that.
 		if ( !url.match( URL_REGEXP ) ) {
+			urlRange.detach();
+
 			return;
 		}
 
 		// If the URL represents a media, let's use it.
 		if ( !mediaRegistry.hasMedia( url ) ) {
+			urlRange.detach();
+
 			return;
 		}
 
@@ -141,6 +145,8 @@ export default class AutoMediaEmbed extends Plugin {
 
 		// Do not anything if media element cannot be inserted at the current position (#47).
 		if ( !mediaEmbedCommand.isEnabled ) {
+			urlRange.detach();
+
 			return;
 		}
 
@@ -153,6 +159,7 @@ export default class AutoMediaEmbed extends Plugin {
 				this._timeoutId = null;
 
 				writer.remove( urlRange );
+				urlRange.detach();
 
 				let insertionPosition;
 

+ 34 - 0
packages/ckeditor5-media-embed/tests/automediaembed.js

@@ -17,6 +17,7 @@ import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Image from '@ckeditor/ckeditor5-image/src/image';
 import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
+import Table from '@ckeditor/ckeditor5-table/src/table';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
@@ -573,6 +574,39 @@ describe( 'AutoMediaEmbed - integration', () => {
 		} );
 	} );
 
+	it( 'should detach LiveRange', async () => {
+		const editor = await ClassicTestEditor.create( editorElement, {
+			plugins: [ MediaEmbed, AutoMediaEmbed, Link, List, Bold, Typing, Image, ImageCaption, Table ]
+		} );
+
+		setData(
+			editor.model,
+			'<table>' +
+				'<tableRow>' +
+					'[<tableCell><paragraph>foo</paragraph></tableCell>]' +
+					'[<tableCell><paragraph>bar</paragraph></tableCell>]' +
+				'</tableRow>' +
+			'</table>'
+		);
+
+		pasteHtml( editor, '<table><tr><td>one</td><td>two</td></tr></table>' );
+
+		expect( getData( editor.model, { withoutSelection: true } ) ).to.equal(
+			'<table>' +
+				'<tableRow>' +
+					'<tableCell><paragraph>one</paragraph></tableCell>' +
+					'<tableCell><paragraph>two</paragraph></tableCell>' +
+				'</tableRow>' +
+			'</table>'
+		);
+
+		expect( () => {
+			editor.setData( '' );
+		} ).not.to.throw();
+
+		await editor.destroy();
+	} );
+
 	function simulateTyping( text ) {
 		// While typing, every character is an atomic change.
 		text.split( '' ).forEach( character => {

+ 12 - 3
packages/ckeditor5-table/src/commands/setheadercolumncommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import { findAncestor, isHeadingColumnCell, updateNumericAttribute } from './utils';
-import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils';
+import { getColumnIndexes, getSelectionAffectedTableCells, getHorizontallyOverlappingCells, splitVertically } from '../utils';
 
 /**
  * The header column command.
@@ -69,12 +69,21 @@ export default class SetHeaderColumnCommand extends Command {
 
 		const model = this.editor.model;
 		const selectedCells = getSelectionAffectedTableCells( model.document.selection );
-		const { first, last } = getColumnIndexes( selectedCells );
+		const table = findAncestor( 'table', selectedCells[ 0 ] );
 
+		const { first, last } = getColumnIndexes( selectedCells );
 		const headingColumnsToSet = this.value ? first : last + 1;
 
 		model.change( writer => {
-			const table = findAncestor( 'table', selectedCells[ 0 ] );
+			if ( headingColumnsToSet ) {
+				// Changing heading columns requires to check if any of a heading cell is overlapping horizontally the table head.
+				// Any table cell that has a colspan attribute > 1 will not exceed the table head so we need to fix it in columns before.
+				const overlappingCells = getHorizontallyOverlappingCells( table, headingColumnsToSet );
+
+				for ( const { cell, column } of overlappingCells ) {
+					splitVertically( cell, column, headingColumnsToSet, writer );
+				}
+			}
 
 			updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );
 		} );

+ 5 - 79
packages/ckeditor5-table/src/commands/setheaderrowcommand.js

@@ -9,9 +9,8 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
-import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './utils';
-import { getRowIndexes, getSelectionAffectedTableCells } from '../utils';
-import TableWalker from '../tablewalker';
+import { findAncestor, updateNumericAttribute } from './utils';
+import { getVerticallyOverlappingCells, getRowIndexes, getSelectionAffectedTableCells, splitHorizontally } from '../utils';
 
 /**
  * The header row command.
@@ -77,9 +76,10 @@ export default class SetHeaderRowCommand extends Command {
 			if ( headingRowsToSet ) {
 				// Changing heading rows requires to check if any of a heading cell is overlapping vertically the table head.
 				// Any table cell that has a rowspan attribute > 1 will not exceed the table head so we need to fix it in rows below.
-				const cellsToSplit = getOverlappingCells( table, headingRowsToSet, currentHeadingRows );
+				const startRow = headingRowsToSet > currentHeadingRows ? currentHeadingRows : 0;
+				const overlappingCells = getVerticallyOverlappingCells( table, headingRowsToSet, startRow );
 
-				for ( const cell of cellsToSplit ) {
+				for ( const { cell } of overlappingCells ) {
 					splitHorizontally( cell, headingRowsToSet, writer );
 				}
 			}
@@ -102,77 +102,3 @@ export default class SetHeaderRowCommand extends Command {
 		return !!headingRows && tableCell.parent.index < headingRows;
 	}
 }
-
-// Returns cells that span beyond the new heading section.
-//
-// @param {module:engine/model/element~Element} table The table to check.
-// @param {Number} headingRowsToSet New heading rows attribute.
-// @param {Number} currentHeadingRows Current heading rows attribute.
-// @returns {Array.<module:engine/model/element~Element>}
-function getOverlappingCells( table, headingRowsToSet, currentHeadingRows ) {
-	const cellsToSplit = [];
-
-	const startAnalysisRow = headingRowsToSet > currentHeadingRows ? currentHeadingRows : 0;
-	// We're analyzing only when headingRowsToSet > 0.
-	const endAnalysisRow = headingRowsToSet - 1;
-
-	const tableWalker = new TableWalker( table, { startRow: startAnalysisRow, endRow: endAnalysisRow } );
-
-	for ( const { row, rowspan, cell } of tableWalker ) {
-		if ( rowspan > 1 && row + rowspan > headingRowsToSet ) {
-			cellsToSplit.push( cell );
-		}
-	}
-
-	return cellsToSplit;
-}
-
-// Splits the table cell horizontally.
-//
-// @param {module:engine/model/element~Element} tableCell
-// @param {Number} headingRows
-// @param {module:engine/model/writer~Writer} writer
-function splitHorizontally( tableCell, headingRows, writer ) {
-	const tableRow = tableCell.parent;
-	const table = tableRow.parent;
-	const rowIndex = tableRow.index;
-
-	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) );
-	const newRowspan = headingRows - rowIndex;
-
-	const attributes = {};
-
-	const spanToSet = rowspan - newRowspan;
-
-	if ( spanToSet > 1 ) {
-		attributes.rowspan = spanToSet;
-	}
-
-	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
-
-	if ( colspan > 1 ) {
-		attributes.colspan = colspan;
-	}
-
-	const startRow = table.getChildIndex( tableRow );
-	const endRow = startRow + newRowspan;
-	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeSpanned: true } ) ];
-
-	let columnIndex;
-
-	for ( const { row, column, cell, cellIndex } of tableMap ) {
-		if ( cell === tableCell && columnIndex === undefined ) {
-			columnIndex = column;
-		}
-
-		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
-			const tableRow = table.getChild( row );
-			const tableCellPosition = writer.createPositionAt( tableRow, cellIndex );
-
-			createEmptyTableCell( writer, tableCellPosition, attributes );
-		}
-	}
-
-	// Update the rowspan attribute after updating table.
-	updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );
-}

+ 252 - 33
packages/ckeditor5-table/src/tableclipboard.js

@@ -11,7 +11,16 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import TableSelection from './tableselection';
 import TableWalker from './tablewalker';
-import { getColumnIndexes, getRowIndexes, getSelectionAffectedTableCells, isSelectionRectangular } from './utils';
+import {
+	getColumnIndexes,
+	getVerticallyOverlappingCells,
+	getRowIndexes,
+	getSelectionAffectedTableCells,
+	getHorizontallyOverlappingCells,
+	isSelectionRectangular,
+	splitHorizontally,
+	splitVertically
+} from './utils';
 import { findAncestor } from './commands/utils';
 import { cropTableToDimensions } from './tableselection/croptable';
 import TableUtils from './tableutils';
@@ -110,9 +119,6 @@ export default class TableClipboard extends Plugin {
 			return;
 		}
 
-		// Content table to which we insert a pasted table.
-		const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] );
-
 		// We might need to crop table before inserting so reference might change.
 		let pastedTable = getTableIfOnlyTableInContent( content );
 
@@ -123,32 +129,70 @@ export default class TableClipboard extends Plugin {
 		// Override default model.insertContent() handling at this point.
 		evt.stop();
 
-		const pasteWidth = tableUtils.getColumns( pastedTable );
-		const pasteHeight = tableUtils.getRows( pastedTable );
-
 		model.change( writer => {
-			let { first: firstColumnOfSelection, last: lastColumnOfSelection } = getColumnIndexes( selectedTableCells );
-			let { first: firstRowOfSelection, last: lastRowOfSelection } = getRowIndexes( selectedTableCells );
+			const columnIndexes = getColumnIndexes( selectedTableCells );
+			const rowIndexes = getRowIndexes( selectedTableCells );
+
+			let { first: firstColumnOfSelection, last: lastColumnOfSelection } = columnIndexes;
+			let { first: firstRowOfSelection, last: lastRowOfSelection } = rowIndexes;
+
+			const pasteHeight = tableUtils.getRows( pastedTable );
+			const pasteWidth = tableUtils.getColumns( pastedTable );
 
-			if ( selectedTableCells.length == 1 ) {
+			// Content table to which we insert a pasted table.
+			const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] );
+
+			// Single cell selected - expand selection to pasted table dimensions.
+			const shouldExpandSelection = selectedTableCells.length === 1;
+
+			if ( shouldExpandSelection ) {
 				lastRowOfSelection += pasteHeight - 1;
 				lastColumnOfSelection += pasteWidth - 1;
 
 				expandTableSize( selectedTable, lastRowOfSelection + 1, lastColumnOfSelection + 1, writer, tableUtils );
 			}
 
-			// Currently not handled. The selected table content should be trimmed to a rectangular selection.
-			// See: https://github.com/ckeditor/ckeditor5/issues/6122.
-			else if ( !isSelectionRectangular( selectedTableCells, tableUtils ) ) {
-				// @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Selection is not rectangular (non-mergeable).' );
+			// In case of expanding selection we do not reset the selection so in this case we will always try to fix selection
+			// like in the case of a non-rectangular area. This might be fixed by re-setting selected cells array but this shortcut is safe.
+			if ( shouldExpandSelection || !isSelectionRectangular( selectedTableCells, tableUtils ) ) {
+				const splitDimensions = {
+					firstRow: firstRowOfSelection,
+					lastRow: lastRowOfSelection,
+					firstColumn: firstColumnOfSelection,
+					lastColumn: lastColumnOfSelection
+				};
 
-				return;
+				// For a non-rectangular selection (ie in which some cells sticks out from a virtual selection rectangle) we need to create
+				// a table layout that has a rectangular selection. This will split cells so the selection become rectangular.
+				// Beyond this point we will operate on fixed content table.
+				splitCellsToRectangularSelection( selectedTable, splitDimensions, writer );
+			}
+			// However a selected table fragment might be invalid if examined alone. Ie such table fragment:
+			//
+			//    +---+---+---+---+
+			//  0 | a | b | c | d |
+			//    +   +   +---+---+
+			//  1 |   | e | f | g |
+			//    +   +---+   +---+
+			//  2 |   | h |   | i | <- last row, each cell has rowspan = 2,
+			//    +   +   +   +   +    so we need to return 3, not 2
+			//  3 |   |   |   |   |
+			//    +---+---+---+---+
+			//
+			// is invalid as the cells "h" and "i" have rowspans.
+			// This case needs only adjusting the selection dimension as the rest of the algorithm operates on empty slots also.
+			else {
+				lastRowOfSelection = adjustLastRowIndex( selectedTable, rowIndexes, columnIndexes );
+				lastColumnOfSelection = adjustLastColumnIndex( selectedTable, rowIndexes, columnIndexes );
 			}
 
+			// Beyond this point we operate on a fixed content table with rectangular selection and proper last row/column values.
+
 			const selectionHeight = lastRowOfSelection - firstRowOfSelection + 1;
 			const selectionWidth = lastColumnOfSelection - firstColumnOfSelection + 1;
 
 			// The if below is temporal and will be removed when handling this case.
+			// This if to be removed as handling of replicating cells should be done in replaceSelectedCellsWithPasted().
 			// See: https://github.com/ckeditor/ckeditor5/issues/6769.
 			if ( selectionHeight > pasteHeight || selectionWidth > pasteWidth ) {
 				// @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Pasted table is smaller than selection area.' );
@@ -156,17 +200,21 @@ export default class TableClipboard extends Plugin {
 				return;
 			}
 
-			// Crop pasted table if it extends selection area.
-			if ( selectionHeight < pasteHeight || selectionWidth < pasteWidth ) {
-				const cropDimensions = {
-					startRow: 0,
-					startColumn: 0,
-					endRow: selectionHeight - 1,
-					endColumn: selectionWidth - 1
-				};
+			// Crop pasted table if:
+			// - Pasted table dimensions exceeds selection area.
+			// - Pasted table has broken layout (ie some cells sticks out by the table dimensions established by the first and last row).
+			//
+			// Note: The table dimensions are established by the width of the first row and the total number of rows.
+			// It is possible to programmatically create a table that has rows which would have cells anchored beyond first row width but
+			// such table will not be created by other editing solutions.
+			const cropDimensions = {
+				startRow: 0,
+				startColumn: 0,
+				endRow: Math.min( selectionHeight - 1, pasteHeight - 1 ),
+				endColumn: Math.min( selectionWidth - 1, pasteWidth - 1 )
+			};
 
-				pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer, tableUtils );
-			}
+			pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer, tableUtils );
 
 			const selectionDimensions = {
 				firstColumnOfSelection,
@@ -196,8 +244,9 @@ export default class TableClipboard extends Plugin {
 // @param {module:engine/model/writer~Writer} writer
 function replaceSelectedCellsWithPasted( pastedTable, selectedTable, selectionDimensions, writer ) {
 	const {
-		firstColumnOfSelection, lastColumnOfSelection, selectionWidth,
-		firstRowOfSelection, lastRowOfSelection, selectionHeight
+		firstColumnOfSelection, lastColumnOfSelection,
+		firstRowOfSelection, lastRowOfSelection,
+		selectionWidth, selectionHeight
 	} = selectionDimensions;
 
 	// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
@@ -272,24 +321,24 @@ function replaceSelectedCellsWithPasted( pastedTable, selectedTable, selectionDi
 	writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
 }
 
-// Expand table (in place) to expected size (rows and columns).
-function expandTableSize( table, rows, columns, writer, tableUtils ) {
+// Expand table (in place) to expected size.
+function expandTableSize( table, expectedHeight, expectedWidth, writer, tableUtils ) {
 	const tableWidth = tableUtils.getColumns( table );
 	const tableHeight = tableUtils.getRows( table );
 
-	if ( columns > tableWidth ) {
+	if ( expectedWidth > tableWidth ) {
 		tableUtils.insertColumns( table, {
 			batch: writer.batch,
 			at: tableWidth,
-			columns: columns - tableWidth
+			columns: expectedWidth - tableWidth
 		} );
 	}
 
-	if ( rows > tableHeight ) {
+	if ( expectedHeight > tableHeight ) {
 		tableUtils.insertRows( table, {
 			batch: writer.batch,
 			at: tableHeight,
-			rows: rows - tableHeight
+			rows: expectedHeight - tableHeight
 		} );
 	}
 }
@@ -349,3 +398,173 @@ function createLocationMap( table, width, height ) {
 
 	return map;
 }
+
+// Make selected cells rectangular by splitting the cells that stand out from a rectangular selection.
+//
+// In the table below a selection is shown with "::" and slots with anchor cells are named.
+//
+// +----+----+----+----+----+                    +----+----+----+----+----+
+// | 00 | 01 | 02 | 03      |                    | 00 | 01 | 02 | 03      |
+// +    +----+    +----+----+                    |    ::::::::::::::::----+
+// |    | 11 |    | 13 | 14 |                    |    ::11 |    | 13:: 14 |    <- first row
+// +----+----+    +    +----+                    +----::---|    |   ::----+
+// | 20 | 21 |    |    | 24 |   select cells:    | 20 ::21 |    |   :: 24 |
+// +----+----+    +----+----+     11 -> 33       +----::---|    |---::----+
+// | 30      |    | 33 | 34 |                    | 30 ::   |    | 33:: 34 |    <- last row
+// +         +    +----+    +                    |    ::::::::::::::::    +
+// |         |    | 43 |    |                    |         |    | 43 |    |
+// +----+----+----+----+----+                    +----+----+----+----+----+
+//                                                      ^          ^
+//                                                     first & last columns
+//
+// Will update table to:
+//
+//                       +----+----+----+----+----+
+//                       | 00 | 01 | 02 | 03      |
+//                       +    +----+----+----+----+
+//                       |    | 11 |    | 13 | 14 |
+//                       +----+----+    +    +----+
+//                       | 20 | 21 |    |    | 24 |
+//                       +----+----+    +----+----+
+//                       | 30 |    |    | 33 | 34 |
+//                       +    +----+----+----+    +
+//                       |    |    |    | 43 |    |
+//                       +----+----+----+----+----+
+//
+// In th example above:
+// - Cell "02" which have `rowspan = 4` must be trimmed at first and at after last row.
+// - Cell "03" which have `rowspan = 2` and `colspan = 2` must be trimmed at first column and after last row.
+// - Cells "00", "03" & "30" which cannot be cut by this algorithm as they are outside the trimmed area.
+// - Cell "13" cannot be cut as it is inside the trimmed area.
+function splitCellsToRectangularSelection( table, dimensions, writer ) {
+	const { firstRow, lastRow, firstColumn, lastColumn } = dimensions;
+
+	const rowIndexes = { first: firstRow, last: lastRow };
+	const columnIndexes = { first: firstColumn, last: lastColumn };
+
+	// 1. Split cells vertically in two steps as first step might create cells that needs to split again.
+	doVerticalSplit( table, firstColumn, rowIndexes, writer );
+	doVerticalSplit( table, lastColumn + 1, rowIndexes, writer );
+
+	// 2. Split cells horizontally in two steps as first step might create cells that needs to split again.
+	doHorizontalSplit( table, firstRow, columnIndexes, writer );
+	doHorizontalSplit( table, lastRow + 1, columnIndexes, writer, firstRow );
+}
+
+function doHorizontalSplit( table, splitRow, limitColumns, writer, startRow = 0 ) {
+	// If selection starts at first row then no split is needed.
+	if ( splitRow < 1 ) {
+		return;
+	}
+
+	const overlappingCells = getVerticallyOverlappingCells( table, splitRow, startRow );
+
+	// Filter out cells that are not touching insides of the rectangular selection.
+	const cellsToSplit = overlappingCells.filter( ( { column, colspan } ) => isAffectedBySelection( column, colspan, limitColumns ) );
+
+	for ( const { cell } of cellsToSplit ) {
+		splitHorizontally( cell, splitRow, writer );
+	}
+}
+
+function doVerticalSplit( table, splitColumn, limitRows, writer ) {
+	// If selection starts at first column then no split is needed.
+	if ( splitColumn < 1 ) {
+		return;
+	}
+
+	const overlappingCells = getHorizontallyOverlappingCells( table, splitColumn );
+
+	// Filter out cells that are not touching insides of the rectangular selection.
+	const cellsToSplit = overlappingCells.filter( ( { row, rowspan } ) => isAffectedBySelection( row, rowspan, limitRows ) );
+
+	for ( const { cell, column } of cellsToSplit ) {
+		splitVertically( cell, column, splitColumn, writer );
+	}
+}
+
+// Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).
+//
+// The same check is used for row as for column.
+function isAffectedBySelection( index, span, limit ) {
+	const endIndex = index + span - 1;
+	const { first, last } = limit;
+
+	const isInsideSelection = index >= first && index <= last;
+	const overlapsSelectionFromOutside = index < first && endIndex >= first;
+
+	return isInsideSelection || overlapsSelectionFromOutside;
+}
+
+// Returns adjusted last row index if selection covers part of a row with empty slots (spanned by other cells).
+// The rowIndexes.last is equal to last row index but selection might be bigger.
+//
+// This happens *only* on rectangular selection so we analyze a case like this:
+//
+//    +---+---+---+---+
+//  0 | a | b | c | d |
+//    +   +   +---+---+
+//  1 |   | e | f | g |
+//    +   +---+   +---+
+//  2 |   | h |   | i | <- last row, each cell has rowspan = 2,
+//    +   +   +   +   +    so we need to return 3, not 2
+//  3 |   |   |   |   |
+//    +---+---+---+---+
+function adjustLastRowIndex( table, rowIndexes, columnIndexes ) {
+	const tableIterator = new TableWalker( table, {
+		startRow: rowIndexes.last,
+		endRow: rowIndexes.last
+	} );
+
+	const lastRowMap = Array.from( tableIterator ).filter( ( { column } ) => {
+		// Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785.
+		return columnIndexes.first <= column && column <= columnIndexes.last;
+	} );
+
+	const everyCellHasSingleRowspan = lastRowMap.every( ( { rowspan } ) => rowspan === 1 );
+
+	// It is a "flat" row, so the last row index is OK.
+	if ( everyCellHasSingleRowspan ) {
+		return rowIndexes.last;
+	}
+
+	// Otherwise get any cell's rowspan and adjust the last row index.
+	const rowspanAdjustment = lastRowMap[ 0 ].rowspan - 1;
+	return rowIndexes.last + rowspanAdjustment;
+}
+
+// Returns adjusted last column index if selection covers part of a column with empty slots (spanned by other cells).
+// The columnIndexes.last is equal to last column index but selection might be bigger.
+//
+// This happens *only* on rectangular selection so we analyze a case like this:
+//
+//   0   1   2   3
+// +---+---+---+---+
+// | a             |
+// +---+---+---+---+
+// | b | c | d     |
+// +---+---+---+---+
+// | e     | f     |
+// +---+---+---+---+
+// | g | h         |
+// +---+---+---+---+
+//           ^
+//          last column, each cell has colspan = 2, so we need to return 3, not 2
+function adjustLastColumnIndex( table, rowIndexes, columnIndexes ) {
+	const lastColumnMap = Array.from( new TableWalker( table, {
+		startRow: rowIndexes.first,
+		endRow: rowIndexes.last,
+		column: columnIndexes.last
+	} ) );
+
+	const everyCellHasSingleColspan = lastColumnMap.every( ( { colspan } ) => colspan === 1 );
+
+	// It is a "flat" column, so the last column index is OK.
+	if ( everyCellHasSingleColspan ) {
+		return columnIndexes.last;
+	}
+
+	// Otherwise get any cell's colspan and adjust the last column index.
+	const colspanAdjustment = lastColumnMap[ 0 ].colspan - 1;
+	return columnIndexes.last + colspanAdjustment;
+}

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

@@ -8,7 +8,7 @@
  */
 
 import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
-import { findAncestor } from './commands/utils';
+import { createEmptyTableCell, findAncestor, updateNumericAttribute } from './commands/utils';
 import TableWalker from './tablewalker';
 
 /**
@@ -188,7 +188,7 @@ export function getColumnIndexes( tableCells ) {
  *   │ a │ b │ c │ d │
  *   ├───┴───┼───┤   │
  *   │ e     │ f │   │
- *          ├───┼───┤
+ *          ├───┼───┤
  *   │       │ g │ h │
  *   └───────┴───┴───┘
  *
@@ -247,6 +247,171 @@ export function isSelectionRectangular( selectedTableCells, tableUtils ) {
 	return areaOfValidSelection == areaOfSelectedCells;
 }
 
+/**
+ * Returns slot info of cells that starts above and overlaps a given row.
+ *
+ * In a table below, passing `overlapRow = 3`
+ *
+ *       ┌───┬───┬───┬───┬───┐
+ *    0  │ a │ b │ c │ d │ e │
+ *       │   ├───┼───┼───┼───┤
+ *    1  │   │ f │ g │ h │ i │
+ *       ├───┤   ├───┼───┤   │
+ *    2  │ j │   │ k │ l │   │
+ *       │   │   │   ├───┼───┤
+ *    3  │   │   │   │ m │ n │  <- overlap row to check
+ *       ├───┼───┤   │   ├───│
+ *    4  │ o │ p │   │   │ q │
+ *       └───┴───┴───┴───┴───┘
+ *
+ * will return slot info for cells: "j", "f", "k".
+ *
+ * @param {module:engine/model/element~Element} table The table to check.
+ * @param {Number} overlapRow The index of the row to check.
+ * @param {Number} [startRow=0] A row to start analysis. Use it when it is known that the cells above that row will not overlap.
+ * @returns {Array.<module:table/tablewalker~TableWalkerValue>}
+ */
+export function getVerticallyOverlappingCells( table, overlapRow, startRow = 0 ) {
+	const cells = [];
+
+	const tableWalker = new TableWalker( table, { startRow, endRow: overlapRow - 1 } );
+
+	for ( const slotInfo of tableWalker ) {
+		const { row, rowspan } = slotInfo;
+		const cellEndRow = row + rowspan - 1;
+
+		if ( row < overlapRow && overlapRow <= cellEndRow ) {
+			cells.push( slotInfo );
+		}
+	}
+
+	return cells;
+}
+
+/**
+ * Splits the table cell horizontally.
+ *
+ * @param {module:engine/model/element~Element} tableCell
+ * @param {Number} splitRow
+ * @param {module:engine/model/writer~Writer} writer
+ */
+export function splitHorizontally( tableCell, splitRow, writer ) {
+	const tableRow = tableCell.parent;
+	const table = tableRow.parent;
+	const rowIndex = tableRow.index;
+
+	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) );
+	const newRowspan = splitRow - rowIndex;
+
+	const newCellAttributes = {};
+	const newCellRowSpan = rowspan - newRowspan;
+
+	if ( newCellRowSpan > 1 ) {
+		newCellAttributes.rowspan = newCellRowSpan;
+	}
+
+	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+
+	if ( colspan > 1 ) {
+		newCellAttributes.colspan = colspan;
+	}
+
+	const startRow = rowIndex;
+	const endRow = startRow + newRowspan;
+	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeSpanned: true } ) ];
+
+	let columnIndex;
+
+	for ( const { row, column, cell, cellIndex } of tableMap ) {
+		if ( cell === tableCell && columnIndex === undefined ) {
+			columnIndex = column;
+		}
+
+		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
+			const tableRow = table.getChild( row );
+			const tableCellPosition = writer.createPositionAt( tableRow, cellIndex );
+
+			createEmptyTableCell( writer, tableCellPosition, newCellAttributes );
+		}
+	}
+
+	// Update the rowspan attribute after updating table.
+	updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );
+}
+
+/**
+ * Returns slot info of cells that starts before and overlaps a given column.
+ *
+ * In a table below, passing `overlapColumn = 3`
+ *
+ *      0   1   2   3   4
+ *    ┌───────┬───────┬───┐
+ *    │ a     │ b     │ c │
+ *    │───┬───┴───────┼───┤
+ *    │ d │ e         │ f │
+ *    ├───┼───┬───────┴───┤
+ *    │ g │ h │ i         │
+ *    ├───┼───┼───┬───────┤
+ *    │ j │ k │ l │ m     │
+ *    ├───┼───┴───┼───┬───┤
+ *    │ n │ o     │ p │ q │
+ *    └───┴───────┴───┴───┘
+ *                  ^
+ *                  Overlap column to check
+ *
+ * will return slot info for cells: "b", "e", "i".
+ *
+ * @param {module:engine/model/element~Element} table The table to check.
+ * @param {Number} overlapColumn The index of the column to check.
+ * @returns {Array.<module:table/tablewalker~TableWalkerValue>}
+ */
+export function getHorizontallyOverlappingCells( table, overlapColumn ) {
+	const cellsToSplit = [];
+
+	const tableWalker = new TableWalker( table );
+
+	for ( const slotInfo of tableWalker ) {
+		const { column, colspan } = slotInfo;
+		const cellEndColumn = column + colspan - 1;
+
+		if ( column < overlapColumn && overlapColumn <= cellEndColumn ) {
+			cellsToSplit.push( slotInfo );
+		}
+	}
+
+	return cellsToSplit;
+}
+
+/**
+ * Splits the table cell vertically.
+ *
+ * @param {module:engine/model/element~Element} tableCell
+ * @param {Number} columnIndex The table cell column index.
+ * @param {Number} splitColumn The index of column to split cell on.
+ * @param {module:engine/model/writer~Writer} writer
+ */
+export function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
+	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) );
+	const newColspan = splitColumn - columnIndex;
+
+	const newCellAttributes = {};
+	const newCellColSpan = colspan - newColspan;
+
+	if ( newCellColSpan > 1 ) {
+		newCellAttributes.colspan = newCellColSpan;
+	}
+
+	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
+
+	if ( rowspan > 1 ) {
+		newCellAttributes.rowspan = rowspan;
+	}
+
+	createEmptyTableCell( writer, writer.createPositionAfter( tableCell ), newCellAttributes );
+	// Update the colspan attribute after updating table.
+	updateNumericAttribute( 'colspan', newColspan, tableCell, writer );
+}
+
 // Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
 function getFirstLastIndexesObject( indexes ) {
 	const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );

+ 104 - 2
packages/ckeditor5-table/tests/commands/setheadercolumncommand.js

@@ -198,6 +198,18 @@ describe( 'SetHeaderColumnCommand', () => {
 			], { headingColumns: 1 } ) );
 		} );
 
+		it( 'should remove "headingColumns" attribute from table if no value was given', () => {
+			setData( model, modelTable( [
+				[ '[]00', '01', '02', '03' ]
+			], { headingColumns: 3 } ) );
+
+			command.execute();
+
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '[]00', '01', '02', '03' ]
+			] ) );
+		} );
+
 		describe( 'multi-cell selection', () => {
 			describe( 'setting header', () => {
 				it( 'should set it correctly in a middle of single-row, multiple cell selection', () => {
@@ -404,7 +416,7 @@ describe( 'SetHeaderColumnCommand', () => {
 			], { headingColumns: 2 } ) );
 		} );
 
-		it( 'should respect forceValue parameter #1', () => {
+		it( 'should respect forceValue parameter (forceValue=true)', () => {
 			setData( model, modelTable( [
 				[ '00', '01[]', '02', '03' ]
 			], { headingColumns: 3 } ) );
@@ -416,7 +428,7 @@ describe( 'SetHeaderColumnCommand', () => {
 			], { headingColumns: 3 } ) );
 		} );
 
-		it( 'should respect forceValue parameter #2', () => {
+		it( 'should respect forceValue parameter (forceValue=false)', () => {
 			setData( model, modelTable( [
 				[ '00', '01[]', '02', '03' ]
 			], { headingColumns: 1 } ) );
@@ -427,5 +439,95 @@ describe( 'SetHeaderColumnCommand', () => {
 				[ '00', '01[]', '02', '03' ]
 			], { headingColumns: 1 } ) );
 		} );
+
+		it( 'should fix col-spanned cells on the edge of an table heading columns section', () => {
+			// +----+----+----+
+			// | 00 | 01      |
+			// +----+         +
+			// | 10 |         |
+			// +----+----+----+
+			// | 20 | 21 | 22 |
+			// +----+----+----+
+			//      ^-- heading columns
+			setData( model, modelTable( [
+				[ '00', { contents: '[]01', colspan: 2, rowspan: 2 } ],
+				[ '10' ],
+				[ '20', '21', '22' ]
+			], { headingColumns: 1 } ) );
+
+			command.execute();
+
+			// +----+----+----+
+			// | 00 | 01 |    |
+			// +----+    +    +
+			// | 10 |    |    |
+			// +----+----+----+
+			// | 20 | 21 | 22 |
+			// +----+----+----+
+			//           ^-- heading columns
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '00', { contents: '[]01', rowspan: 2 }, { contents: '', rowspan: 2 } ],
+				[ '10' ],
+				[ '20', '21', '22' ]
+			], { headingColumns: 2 } ) );
+		} );
+
+		it( 'should split to at most 2 table cells when fixing col-spanned cells on the edge of an table heading columns section', () => {
+			// +----+----+----+----+----+----+
+			// | 00 | 01                     |
+			// +----+                        +
+			// | 10 |                        |
+			// +----+----+----+----+----+----+
+			// | 20 | 21 | 22 | 23 | 24 | 25 |
+			// +----+----+----+----+----+----+
+			//      ^-- heading columns
+			setData( model, modelTable( [
+				[ '00', { contents: '01', colspan: 5, rowspan: 2 } ],
+				[ '10' ],
+				[ '20', '21', '22[]', '23', '24', '25' ]
+			], { headingColumns: 1 } ) );
+
+			command.execute();
+
+			// +----+----+----+----+----+----+
+			// | 00 | 01      |              |
+			// +----+         +              +
+			// | 10 |         |              |
+			// +----+----+----+----+----+----+
+			// | 20 | 21 | 22 | 23 | 24 | 25 |
+			// +----+----+----+----+----+----+
+			//                ^-- heading columns
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '00', { contents: '01', colspan: 2, rowspan: 2 }, { contents: '', colspan: 3, rowspan: 2 } ],
+				[ '10' ],
+				[ '20', '21', '22[]', '23', '24', '25' ]
+			], { headingColumns: 3 } ) );
+		} );
+
+		it( 'should fix col-spanned cells on the edge of an table heading columns section when creating section', () => {
+			// +----+----+
+			// | 00      |
+			// +----+----+
+			// | 10 | 11 |
+			// +----+----+
+			//           ^-- heading columns
+			setData( model, modelTable( [
+				[ { contents: '00', colspan: 2 } ],
+				[ '10', '[]11' ]
+			], { headingColumns: 2 } ) );
+
+			command.execute();
+
+			// +----+----+
+			// | 00 |    |
+			// +----+----+
+			// | 10 | 11 |
+			// +----+----+
+			//      ^-- heading columns
+			assertEqualMarkup( getData( model ), modelTable( [
+				[ '00', '' ],
+				[ '10', '[]11' ]
+			], { headingColumns: 1 } ) );
+		} );
 	} );
 } );

+ 3 - 3
packages/ckeditor5-table/tests/commands/setheaderrowcommand.js

@@ -225,7 +225,7 @@ describe( 'SetHeaderRowCommand', () => {
 			], { headingRows: 1 } ) );
 		} );
 
-		it( 'should unsetset heading rows attribute', () => {
+		it( 'should remove "headingRows" attribute from table if no value was given', () => {
 			setData( model, modelTable( [
 				[ '[]00' ],
 				[ '10' ],
@@ -551,7 +551,7 @@ describe( 'SetHeaderRowCommand', () => {
 			} );
 		} );
 
-		it( 'should respect forceValue parameter #1', () => {
+		it( 'should respect forceValue parameter (forceValue=true)', () => {
 			setData( model, modelTable( [
 				[ '00' ],
 				[ '[]10' ],
@@ -569,7 +569,7 @@ describe( 'SetHeaderRowCommand', () => {
 			], { headingRows: 3 } ) );
 		} );
 
-		it( 'should respect forceValue parameter #2', () => {
+		it( 'should respect forceValue parameter (forceValue=false)', () => {
 			setData( model, modelTable( [
 				[ '00' ],
 				[ '[]10' ],

+ 987 - 87
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -272,33 +272,6 @@ describe( 'table clipboard', () => {
 			] ) );
 		} );
 
-		it( 'should block non-rectangular selection', () => {
-			setModelData( model, modelTable( [
-				[ { contents: '00', colspan: 3 } ],
-				[ '10', '11', '12' ],
-				[ '20', '21', '22' ]
-			] ) );
-
-			tableSelection.setCellSelection(
-				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
-				modelRoot.getNodeByPath( [ 0, 1, 1 ] )
-			);
-
-			// Catches the temporary console log in the CK_DEBUG mode.
-			sinon.stub( console, 'log' );
-
-			pasteTable( [
-				[ 'aa', 'ab' ],
-				[ 'ba', 'bb' ]
-			] );
-
-			assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
-				[ { contents: '00', colspan: 3 } ],
-				[ '10', '11', '12' ],
-				[ '20', '21', '22' ]
-			] ) );
-		} );
-
 		describe( 'single cell selected', () => {
 			beforeEach( () => {
 				setModelData( model, modelTable( [
@@ -792,6 +765,39 @@ describe( 'table clipboard', () => {
 						[ '', '', '', 'ea', 'eb', 'ec', 'ed' ]
 					] ) );
 				} );
+
+				it( 'should fix non-rectangular are on matched table fragment', () => {
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+----+    +
+					// | 10 | 11 |    |
+					// +----+----+----+
+					// | 20      | 22 |
+					// +----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', '01', { contents: '02', rowspan: 2 } ],
+						[ '10', '11[]' ],
+						[ { contents: '20', colspan: 2 }, '22' ]
+					] ) );
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+----+----+
+					// | 10 | aa | ab |
+					// +----+----+----+
+					// | 20 | ba | bb |
+					// +----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02' ],
+						[ '10', 'aa', 'ab' ],
+						[ '20', 'ba', 'bb' ]
+					] ) );
+				} );
 			} );
 		} );
 
@@ -1371,8 +1377,7 @@ describe( 'table clipboard', () => {
 					/* eslint-enable no-multi-spaces */
 				} );
 
-				// TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand).
-				it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => {
+				it( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => {
 					// +----+----+----+----+
 					// | 00 | 01 | 02 | 03 |
 					// +----+----+----+----+
@@ -1389,6 +1394,7 @@ describe( 'table clipboard', () => {
 						[ '30', '31', '32', '33' ]
 					] ) );
 
+					// Select 02 -> 10 (selection 3x3)
 					tableSelection.setCellSelection(
 						modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
 						modelRoot.getNodeByPath( [ 0, 1, 0 ] )
@@ -1414,6 +1420,132 @@ describe( 'table clipboard', () => {
 						[ 0, 0, 0, 0 ]
 					] );
 				} );
+
+				it( 'handles pasting table that has cell with colspan (multiple ending rows in the selection are spanned)', () => {
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+    +    +
+					// | 10 |    |    |
+					// +----+    +    +
+					// | 20 |    |    |
+					// +----+----+----+
+					// | 30 | 31 | 32 |
+					// +----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', { contents: '01', rowspan: 3 }, { contents: '02', rowspan: 3 } ],
+						[ '10' ],
+						[ '20' ],
+						[ '30', '31', '32' ]
+					] ) );
+
+					// Select 01 -> 02 (selection 2x2)
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 0, 2 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ],
+						[ 'ca', 'cb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', 'aa', 'ab' ],
+						[ '10', 'ba', 'bb' ],
+						[ '20', 'ca', 'cb' ],
+						[ '30', '31', '32' ]
+					] ) );
+
+					assertSelectedCells( model, [
+						[ 0, 1, 1 ],
+						[ 0, 1, 1 ],
+						[ 0, 1, 1 ],
+						[ 0, 0, 0 ]
+					] );
+				} );
+
+				it( 'handles pasting table that has cell with colspan (last column in selection is spanned)', () => {
+					// +----+----+----+----+
+					// | 00 | 01      | 03 |
+					// +----+----+----+----+
+					// | 10 | 11      | 13 |
+					// +----+         +----+
+					// | 20 |         | 23 |
+					// +----+----+----+----+
+					// | 30 | 31 | 32 | 33 |
+					// +----+----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', { contents: '01', colspan: 2 }, '03' ],
+						[ '10', { contents: '11', colspan: 2, rowspan: 2 }, '13' ],
+						[ '20', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					// Select 20 -> 01 (selection 3x3)
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ 'ca', 'cb', 'cc' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', 'ac', '03' ],
+						[ 'ba', 'bb', 'bc', '13' ],
+						[ 'ca', 'cb', 'cc', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					assertSelectedCells( model, [
+						[ 1, 1, 1, 0 ],
+						[ 1, 1, 1, 0 ],
+						[ 1, 1, 1, 0 ],
+						[ 0, 0, 0, 0 ]
+					] );
+				} );
+
+				it( 'handles pasting table that has cell with colspan (multiple ending columns in the selection are spanned)', () => {
+					// +----+----+----+----+
+					// | 00 | 01 | 02 | 03 |
+					// +----+----+----+----+
+					// | 10           | 13 |
+					// +----+----+----+----+
+					// | 20           | 23 |
+					// +----+----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', '01', '02', '03' ],
+						[ { contents: '10', colspan: 3 }, '13' ],
+						[ { contents: '20', colspan: 3 }, '23' ]
+					] ) );
+
+					// Select 10 -> 20 (selection 3x2)
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 0 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '03' ],
+						[ 'aa', 'ab', 'ac', '13' ],
+						[ 'ba', 'bb', 'bc', '23' ]
+					] ) );
+
+					assertSelectedCells( model, [
+						[ 0, 0, 0, 0 ],
+						[ 1, 1, 1, 0 ],
+						[ 1, 1, 1, 0 ]
+					] );
+				} );
 			} );
 
 			describe( 'content and paste tables have spans', () => {
@@ -1565,8 +1697,7 @@ describe( 'table clipboard', () => {
 					/* eslint-enable no-multi-spaces */
 				} );
 
-				// TODO: Skipped case - should allow pasting but no tools to compare areas (like in MergeCellsCommand).
-				it.skip( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => {
+				it( 'handles pasting table that has cell with colspan (last row in selection is spanned)', () => {
 					// +----+----+----+----+
 					// | 00 | 01 | 02 | 03 |
 					// +----+----+----+----+
@@ -1583,6 +1714,7 @@ describe( 'table clipboard', () => {
 						[ '30', '31', '32', '33' ]
 					] ) );
 
+					// Select 02 -> 10 (selection 3x3)
 					tableSelection.setCellSelection(
 						modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
 						modelRoot.getNodeByPath( [ 0, 1, 0 ] )
@@ -1602,111 +1734,591 @@ describe( 'table clipboard', () => {
 					] );
 
 					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
-						[ '00', '01', '02', '03' ],
-						[ '10', { colspan: 2, contents: 'aa' }, '13' ],
-						[ '20', 'ba', 'bb', '23' ],
+						[ 'aa', 'ab', 'ac', '03' ],
+						[ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc', '13' ],
+						[ 'cc', '23' ],
 						[ '30', '31', '32', '33' ]
 					] ) );
 
 					/* eslint-disable no-multi-spaces */
 					assertSelectedCells( model, [
-						[ 0, 0, 0, 0 ],
-						[ 0, 1,    0 ],
-						[ 0, 1, 1, 0 ],
+						[ 1, 1, 1, 0 ],
+						[ 1,    1, 0 ],
+						[       1, 0 ],
 						[ 0, 0, 0, 0 ]
 					] );
 					/* eslint-enable no-multi-spaces */
 				} );
-			} );
-		} );
 
-		describe( 'pasted table is bigger than the selected area', () => {
-			describe( 'no spans', () => {
-				it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => {
+				it( 'handles pasting table that has cell with colspan (last column in selection is spanned)', () => {
+					// +----+----+----+----+
+					// | 00 | 01      | 03 |
+					// +----+----+----+----+
+					// | 10 | 11      | 13 |
+					// +----+         +----+
+					// | 20 |         | 23 |
+					// +----+----+----+----+
+					// | 30 | 31 | 32 | 33 |
+					// +----+----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', { contents: '01', colspan: 2 }, '03' ],
+						[ '10', { contents: '11', colspan: 2, rowspan: 2 }, '13' ],
+						[ '20', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					// Select 20 -> 01 (selection 3x3)
 					tableSelection.setCellSelection(
-						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
-						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+						modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 0, 1 ] )
 					);
 
+					// +----+----+----+
+					// | aa | ab | ac |
+					// +----+----+----+
+					// | ba      | bc |
+					// +         +----+
+					// |         | cc |
+					// +----+----+----+
 					pasteTable( [
 						[ 'aa', 'ab', 'ac' ],
-						[ 'ba', 'bb', 'bc' ],
-						[ 'ca', 'cb', 'cc' ]
+						[ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc' ],
+						[ 'cc' ]
 					] );
 
 					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
-						[ 'aa', 'ab', '02', '03' ],
-						[ 'ba', 'bb', '12', '13' ],
-						[ '20', '21', '22', '23' ],
+						[ 'aa', 'ab', 'ac', '03' ],
+						[ { contents: 'ba', colspan: 2, rowspan: 2 }, 'bc', '13' ],
+						[ 'cc', '23' ],
 						[ '30', '31', '32', '33' ]
 					] ) );
 
+					/* eslint-disable no-multi-spaces */
 					assertSelectedCells( model, [
-						[ 1, 1, 0, 0 ],
-						[ 1, 1, 0, 0 ],
-						[ 0, 0, 0, 0 ],
+						[ 1, 1, 1, 0 ],
+						[ 1,    1, 0 ],
+						[       1, 0 ],
 						[ 0, 0, 0, 0 ]
 					] );
+					/* eslint-enable no-multi-spaces */
 				} );
+			} );
 
-				it( 'handles simple table paste to a simple table fragment - at the end of a table', () => {
+			describe( 'non-rectangular content table selection', () => {
+				it( 'should split cells outside the selected area before pasting (rowspan ends in selection)', () => {
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+    +----+
+					// | 10 |    | 12 |
+					// +----+    +----+
+					// | 20 |    | 22 |
+					// +----+    +----+
+					// | 30 |    | 32 |
+					// +----+----+----+
+					// | 40 | 41 | 42 |
+					// +----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', { contents: '01', rowspan: 4 }, '02' ],
+						[ '10', '12' ],
+						[ '20', '22' ],
+						[ '30', '32' ],
+						[ '40', '41', '42' ]
+					] ) );
+
+					// Select 20 -> 32
 					tableSelection.setCellSelection(
-						modelRoot.getNodeByPath( [ 0, 2, 2 ] ),
-						modelRoot.getNodeByPath( [ 0, 3, 3 ] )
+						modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 3, 1 ] )
 					);
 
 					pasteTable( [
 						[ 'aa', 'ab', 'ac' ],
-						[ 'ba', 'bb', 'bc' ],
-						[ 'ca', 'cb', 'cc' ]
+						[ 'ba', 'bb', 'bc' ]
 					] );
 
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+    +----+
+					// | 10 |    | 12 |
+					// +----+----+----+
+					// | aa | ab | ac |
+					// +----+----+----+
+					// | ba | bb | bc |
+					// +----+----+----+
+					// | 40 | 41 | 42 |
+					// +----+----+----+
 					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
-						[ '00', '01', '02', '03' ],
-						[ '10', '11', '12', '13' ],
-						[ '20', '21', 'aa', 'ab' ],
-						[ '30', '31', 'ba', 'bb' ]
+						[ '00', { contents: '01', rowspan: 2 }, '02' ],
+						[ '10', '12' ],
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ '40', '41', '42' ]
 					] ) );
-
-					assertSelectedCells( model, [
-						[ 0, 0, 0, 0 ],
-						[ 0, 0, 0, 0 ],
-						[ 0, 0, 1, 1 ],
-						[ 0, 0, 1, 1 ]
-					] );
 				} );
 
-				it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => {
+				it( 'should split cells outside the selected area before pasting (rowspan ends after the selection)', () => {
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+    +----+
+					// | 10 |    | 12 |
+					// +----+    +----+
+					// | 20 |    | 22 |
+					// +----+    +----+
+					// | 30 |    | 32 |
+					// +----+----+----+
+					// | 40 | 41 | 42 |
+					// +----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', { contents: '01', rowspan: 4 }, '02' ],
+						[ '10', '12' ],
+						[ '20', '22' ],
+						[ '30', '32' ],
+						[ '40', '41', '42' ]
+					] ) );
+
+					// Select 10 -> 22
 					tableSelection.setCellSelection(
-						modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
-						modelRoot.getNodeByPath( [ 0, 2, 2 ] )
+						modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 1 ] )
 					);
 
 					pasteTable( [
 						[ 'aa', 'ab', 'ac' ],
-						[ 'ba', 'bb', 'bc' ],
-						[ 'ca', 'cb', 'cc' ]
+						[ 'ba', 'bb', 'bc' ]
 					] );
 
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+----+----+
+					// | aa | ab | ac |
+					// +----+----+----+
+					// | ba | bb | bc |
+					// +----+----+----+
+					// | 30 |    | 32 |
+					// +----+----+----+
+					// | 40 | 41 | 42 |
+					// +----+----+----+
 					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
-						[ '00', '01', '02', '03' ],
-						[ '10', 'aa', 'ab', '13' ],
-						[ '20', 'ba', 'bb', '23' ],
-						[ '30', '31', '32', '33' ]
+						[ '00', '01', '02' ],
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ '30', '', '32' ],
+						[ '40', '41', '42' ]
 					] ) );
-
-					assertSelectedCells( model, [
-						[ 0, 0, 0, 0 ],
-						[ 0, 1, 1, 0 ],
-						[ 0, 1, 1, 0 ],
-						[ 0, 0, 0, 0 ]
-					] );
 				} );
 
-				it( 'handles paste to a simple row fragment - in the middle of a table', () => {
+				it( 'should split cells inside the selected area before pasting (rowspan ends after the selection)', () => {
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+    +----+
+					// | 10 |    | 12 |
+					// +----+    +----+
+					// | 20 |    | 22 |
+					// +----+    +----+
+					// | 30 |    | 32 |
+					// +----+----+----+
+					// | 40 | 41 | 42 |
+					// +----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', { contents: '01', rowspan: 4 }, '02' ],
+						[ '10', '12' ],
+						[ '20', '22' ],
+						[ '30', '32' ],
+						[ '40', '41', '42' ]
+					] ) );
+
+					// Select 00 -> 12
 					tableSelection.setCellSelection(
-						modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
-						modelRoot.getNodeByPath( [ 0, 1, 2 ] )
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ]
+					] );
+
+					// +----+----+----+
+					// | aa | ab | ac |
+					// +----+----+----+
+					// | ba | bb | bc |
+					// +----+----+----+
+					// | 20 |    | 22 |
+					// +----+    +----+
+					// | 30 |    | 32 |
+					// +----+----+----+
+					// | 40 | 41 | 42 |
+					// +----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ '20', { rowspan: 2, contents: '' }, '22' ],
+						[ '30', '32' ],
+						[ '40', '41', '42' ]
+					] ) );
+				} );
+
+				it( 'should split cells outside the selected area before pasting (colspan ends in selection)', () => {
+					// +----+----+----+----+----+
+					// | 00 | 01 | 02 | 03 | 04 |
+					// +----+----+----+----+----+
+					// | 10                | 14 |
+					// +----+----+----+----+----+
+					// | 20 | 21 | 22 | 23 | 24 |
+					// +----+----+----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', '01', '02', '03', '04' ],
+						[ { contents: '10', colspan: 4 }, '14' ],
+						[ '20', '21', '22', '23', '24' ]
+					] ) );
+
+					// Select 02 -> 23
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 3 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ],
+						[ 'ca', 'cb' ]
+					] );
+
+					// +----+----+----+----+----+
+					// | 00 | 01 | aa | ab | 04 |
+					// +----+----+----+----+----+
+					// | 10      | ba | bb | 14 |
+					// +----+----+----+----+----+
+					// | 20 | 21 | ca | cb | 24 |
+					// +----+----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', 'aa', 'ab', '04' ],
+						[ { contents: '10', colspan: 2 }, 'ba', 'bb', '14' ],
+						[ '20', '21', 'ca', 'cb', '24' ]
+					] ) );
+				} );
+
+				it( 'should split cells outside the selected area before pasting (colspan ends after the selection)', () => {
+					// +----+----+----+----+----+
+					// | 00 | 01 | 02 | 03 | 04 |
+					// +----+----+----+----+----+
+					// | 10                | 14 |
+					// +----+----+----+----+----+
+					// | 20 | 21 | 22 | 23 | 24 |
+					// +----+----+----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', '01', '02', '03', '04' ],
+						[ { contents: '10', colspan: 4 }, '14' ],
+						[ '20', '21', '22', '23', '24' ]
+					] ) );
+
+					// Select 01 -> 22
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 2 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ],
+						[ 'ca', 'cb' ]
+					] );
+
+					// +----+----+----+----+----+
+					// | 00 | aa | ab | 03 | 04 |
+					// +----+----+----+----+----+
+					// | 10 | ba | bb |    | 14 |
+					// +----+----+----+----+----+
+					// | 20 | ca | cb | 23 | 24 |
+					// +----+----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', 'aa', 'ab', '03', '04' ],
+						[ '10', 'ba', 'bb', '', '14' ],
+						[ '20', 'ca', 'cb', '23', '24' ]
+					] ) );
+				} );
+
+				it( 'should split cells inside the selected area before pasting (colspan ends after the selection)', () => {
+					// +----+----+----+----+----+
+					// | 00 | 01 | 02 | 03 | 04 |
+					// +----+----+----+----+----+
+					// | 10                | 14 |
+					// +----+----+----+----+----+
+					// | 20 | 21 | 22 | 23 | 24 |
+					// +----+----+----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', '01', '02', '03', '04' ],
+						[ { contents: '10', colspan: 4 }, '14' ],
+						[ '20', '21', '22', '23', '24' ]
+					] ) );
+
+					// Select 00 -> 21
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ],
+						[ 'ca', 'cb' ]
+					] );
+
+					// +----+----+----+----+----+
+					// | aa | ab | 02 | 03 | 04 |
+					// +----+----+----+----+----+
+					// | ba | bb |         | 14 |
+					// +----+----+----+----+----+
+					// | ca | cb | 22 | 23 | 24 |
+					// +----+----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02', '03', '04' ],
+						[ 'ba', 'bb', { colspan: 2, contents: '' }, '14' ],
+						[ 'ca', 'cb', '22', '23', '24' ]
+					] ) );
+				} );
+
+				it( 'should split cells anchored outside selection rectangle that overlaps selection (above selection)', () => {
+					// +----+----+----+
+					// | 00      | 02 |
+					// +         +----+
+					// |         | 12 |
+					// +----+----+----+
+					// | 20 | 21 | 22 |
+					// +----+----+----+
+					setModelData( model, modelTable( [
+						[ { contents: '00', colspan: 2, rowspan: 2 }, '02' ],
+						[ '12' ],
+						[ '20', '21', '22' ]
+					] ) );
+
+					// Select 21 -> 12
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 2, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 0 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					// +----+----+----+
+					// | 00 |    | 02 |
+					// +    +----+----+
+					// |    | aa | ab |
+					// +----+----+----+
+					// | 20 | ba | bb |
+					// +----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ { contents: '00', rowspan: 2 }, '', '02' ],
+						[ 'aa', 'ab' ],
+						[ '20', 'ba', 'bb' ]
+					] ) );
+				} );
+
+				it( 'should split cells anchored outside selection rectangle that overlaps selection (below selection)', () => {
+					// +----+----+----+
+					// | 00 | 01 | 02 |
+					// +----+----+----+
+					// | 10      | 12 |
+					// +         +----+
+					// |         | 22 |
+					// +----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', '01', '02' ],
+						[ { contents: '10', colspan: 2, rowspan: 2 }, '12' ],
+						[ '22' ]
+					] ) );
+
+					// Select 01 -> 12
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					// +----+----+----+
+					// | 00 | aa | ab |
+					// +----+----+----+
+					// | 10 | ba | bb |
+					// +    +----+----+
+					// |    |    | 22 |
+					// +----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', 'aa', 'ab' ],
+						[ { contents: '10', rowspan: 2 }, 'ba', 'bb' ],
+						[ '', '22' ]
+					] ) );
+				} );
+
+				it( 'should properly handle complex case', () => {
+					// +----+----+----+----+----+----+----+
+					// | 00           | 03 | 04           |
+					// +              +    +----+----+----+
+					// |              |    | 14 | 15 | 16 |
+					// +              +    +----+----+----+
+					// |              |    | 24           |
+					// +----+----+----+----+----+----+----+
+					// | 30 | 31      | 33 | 34 | 35 | 36 |
+					// +    +----+----+    +----+----+----+
+					// |    | 41 | 42 |    | 44 | 45      |
+					// +    +----+----+    +----+         +
+					// |    | 51 | 52 |    | 54 |         |
+					// +----+----+----+    +----+         +
+					// | 60 | 61 | 62 |    | 64 |         |
+					// +----+----+----+----+----+----+----+
+					setModelData( model, modelTable( [
+						[ { contents: '00', colspan: 3, rowspan: 3 }, { contents: '03', rowspan: 3 }, { colspan: 3, contents: '04' } ],
+						[ '14', '15', '16' ],
+						[ { contents: '24', colspan: 3 } ],
+						[
+							{ contents: '30', rowspan: 3 },
+							{ contents: '31', colspan: 2 },
+							{ contents: '33', rowspan: 4 },
+							'34', '35', '36'
+						],
+						[ '41', '42', '44', { contents: '45', colspan: 2, rowspan: 3 } ],
+						[ '51', '52', '54' ],
+						[ '60', '61', '62', '64' ]
+					] ) );
+
+					// Select 42 -> 24 (3x3 selection)
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 4, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 0 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ 'ca', 'cb', 'cc' ]
+					] );
+
+					// +----+----+----+----+----+----+----+
+					// | 00      |    | 03 | 04           |
+					// +         +    +    +----+----+----+
+					// |         |    |    | 14 | 15 | 16 |
+					// +         +----+----+----+----+----+
+					// |         | aa | ab | ac |         |
+					// +----+----+----+----+----+----+----+
+					// | 30 | 31 | ba | bb | bc | 35 | 36 |
+					// +    +----+----+----+----+----+----+
+					// |    | 41 | ca | cb | cc | 45      |
+					// +    +----+----+----+----+         +
+					// |    | 51 | 52 |    | 54 |         |
+					// +----+----+----+    +----+         +
+					// | 60 | 61 | 62 |    | 64 |         |
+					// +----+----+----+----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[
+							{ contents: '00', colspan: 2, rowspan: 3 },
+							{ contents: '', rowspan: 2 },
+							{ contents: '03', rowspan: 2 },
+							{ contents: '04', colspan: 3 }
+						],
+						[ '14', '15', '16' ],
+						[ 'aa', 'ab', 'ac', { contents: '', colspan: 2 } ],
+						[ { contents: '30', rowspan: 3 }, '31', 'ba', 'bb', 'bc', '35', '36' ],
+						[ '41', 'ca', 'cb', 'cc', { contents: '45', colspan: 2, rowspan: 3 } ],
+						[ '51', '52', { contents: '', rowspan: 2 }, '54' ],
+						[ '60', '61', '62', '64' ]
+					] ) );
+				} );
+			} );
+		} );
+
+		describe( 'pasted table is bigger than the selected area', () => {
+			describe( 'no spans', () => {
+				it( 'handles simple table paste to a simple table fragment - at the beginning of a table', () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ 'ca', 'cb', 'cc' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02', '03' ],
+						[ 'ba', 'bb', '12', '13' ],
+						[ '20', '21', '22', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					assertSelectedCells( model, [
+						[ 1, 1, 0, 0 ],
+						[ 1, 1, 0, 0 ],
+						[ 0, 0, 0, 0 ],
+						[ 0, 0, 0, 0 ]
+					] );
+				} );
+
+				it( 'handles simple table paste to a simple table fragment - at the end of a table', () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 2, 2 ] ),
+						modelRoot.getNodeByPath( [ 0, 3, 3 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ 'ca', 'cb', 'cc' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '03' ],
+						[ '10', '11', '12', '13' ],
+						[ '20', '21', 'aa', 'ab' ],
+						[ '30', '31', 'ba', 'bb' ]
+					] ) );
+
+					assertSelectedCells( model, [
+						[ 0, 0, 0, 0 ],
+						[ 0, 0, 0, 0 ],
+						[ 0, 0, 1, 1 ],
+						[ 0, 0, 1, 1 ]
+					] );
+				} );
+
+				it( 'handles simple table paste to a simple table fragment - in the middle of a table', () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 2 ] )
+					);
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac' ],
+						[ 'ba', 'bb', 'bc' ],
+						[ 'ca', 'cb', 'cc' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '03' ],
+						[ '10', 'aa', 'ab', '13' ],
+						[ '20', 'ba', 'bb', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					assertSelectedCells( model, [
+						[ 0, 0, 0, 0 ],
+						[ 0, 1, 1, 0 ],
+						[ 0, 1, 1, 0 ],
+						[ 0, 0, 0, 0 ]
+					] );
+				} );
+
+				it( 'handles paste to a simple row fragment - in the middle of a table', () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 2 ] )
 					);
 
 					pasteTable( [
@@ -1997,6 +2609,294 @@ describe( 'table clipboard', () => {
 				] );
 			} );
 		} );
+
+		describe( 'fixing pasted table broken layout', () => {
+			it( 'should trim pasted cells\' width if they exceeds table width established by the first row', () => {
+				// Select 00 -> 22 (selection 2x3 - equal to expected fixed table)
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+				);
+
+				// +----+----+
+				// | aa | ab |             <- First row establish table width=2.
+				// +----+----+----+----+
+				// | ba | bb           |   <- Cell "bb" sticks out by 2 slots.
+				// +----+----+----+----+
+				// | ca           |        <- Cell "ca" sticks out by 1 slot.
+				// +----+----+----+
+				pasteTable( [
+					[ 'aa', 'ab' ],
+					[ 'ba', { colspan: 3, contents: 'bb' } ],
+					[ { colspan: 3, contents: 'ca' } ]
+				] );
+
+				// +----+----+----+----+
+				// | aa | ab | 02 | 03 |
+				// +----+----+----+----+
+				// | ba | bb | 12 | 13 |
+				// +----+----+----+----+
+				// | ca      | 22 | 23 |
+				// +----+----+----+----+
+				// | 30 | 31 | 32 | 33 |
+				// +----+----+----+----+
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ 'aa', 'ab', '02', '03' ],
+					[ 'ba', 'bb', '12', '13' ],
+					[ { contents: 'ca', colspan: 2 }, '22', '23' ],
+					[ '30', '31', '32', '33' ]
+				] ) );
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1, 1, 0, 0 ],
+					[ 1, 1, 0, 0 ],
+					[ 1,    0, 0 ],
+					[ 0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should trim pasted cells\' height if they exceeds table height established by the last row', () => {
+				// Select 00 -> 12 (selection 3x2 - equal to expected fixed table)
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 2 ] )
+				);
+
+				// +----+----+----+
+				// | aa | ab | ac |
+				// +----+----+    +
+				// | ba | bb |    |   <- Last row establish table height=2.
+				// +----+    +    +
+				//      |    |    |   <- Cell "ac" sticks out by 1 slot.
+				//      +    +----+
+				//      |    |        <- Cell "bb" sticks out by 2 slots.
+				//      +----+
+				pasteTable( [
+					[ 'aa', 'ab', { contents: 'ac', rowspan: 3 } ],
+					[ 'ba', { contents: 'bb', rowspan: 3 } ]
+				] );
+
+				// +----+----+----+----+
+				// | aa | ab | ac | 03 |
+				// +----+----+    +----+
+				// | ba | bb |    | 13 |
+				// +----+----+----+----+
+				// | 20 | 21 | 22 | 23 |
+				// +----+----+----+----+
+				// | 30 | 31 | 32 | 33 |
+				// +----+----+----+----+
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ 'aa', 'ab', { rowspan: 2, contents: 'ac' }, '03' ],
+					[ 'ba', 'bb', '13' ],
+					[ '20', '21', '22', '23' ],
+					[ '30', '31', '32', '33' ]
+				] ) );
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1, 1, 1, 0 ],
+					[ 1, 1,    0 ],
+					[ 0, 0, 0, 0 ],
+					[ 0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should trim pasted cells\' height and width if they exceeds table height and width', () => {
+				// Select 00 -> 11 (selection 2x2 - equal to expected fixed table)
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				// +----+----+
+				// | aa | ab |
+				// +----+----+----+----+
+				// | ba | bb           | <- Cell "bb" sticks out by 2 slots in width and by 1 slot in height.
+				// +----+              +
+				//      |              |
+				//      +----+----+----+
+				pasteTable( [
+					[ 'aa', 'ab' ],
+					[ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ]
+				] );
+
+				// +----+----+----+----+
+				// | aa | ab | 02 | 03 |
+				// +----+----+----+----+
+				// | ba | bb | 12 | 13 |
+				// +----+----+----+----+
+				// | 20 | 21 | 22 | 23 |
+				// +----+----+----+----+
+				// | 30 | 31 | 32 | 33 |
+				// +----+----+----+----+
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ 'aa', 'ab', '02', '03' ],
+					[ 'ba', 'bb', '12', '13' ],
+					[ '20', '21', '22', '23' ],
+					[ '30', '31', '32', '33' ]
+				] ) );
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1, 1, 0, 0 ],
+					[ 1, 1, 0, 0 ],
+					[ 0, 0, 0, 0 ],
+					[ 0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it(
+				'should trim pasted cells\' width if they exceeds pasted table width (pasted height is bigger then selection height)',
+				() => {
+					// Select 00 -> 11 (selection 2x2 - smaller by height than the expected fixed table)
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+					);
+
+					// +----+----+
+					// | aa | ab |             <- First row establish table width=2.
+					// +----+----+----+----+
+					// | ba | bb           |   <- Cell "bb" sticks out by 2 slots.
+					// +----+----+----+----+
+					// | ca           |        <- Cell "ca" sticks out by 1 slot.
+					// +----+----+----+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', { colspan: 3, contents: 'bb' } ],
+						[ { colspan: 3, contents: 'ca' } ]
+					] );
+
+					// +----+----+----+----+
+					// | aa | ab | 02 | 03 |
+					// +----+----+----+----+
+					// | ba | bb | 12 | 13 |
+					// +----+----+----+----+
+					// | 20 | 21 | 22 | 23 |
+					// +----+----+----+----+
+					// | 30 | 31 | 32 | 33 |
+					// +----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02', '03' ],
+						[ 'ba', 'bb', '12', '13' ],
+						[ '20', '21', '22', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					assertSelectedCells( model, [
+						[ 1, 1, 0, 0 ],
+						[ 1, 1, 0, 0 ],
+						[ 0, 0, 0, 0 ],
+						[ 0, 0, 0, 0 ]
+					] );
+				}
+			);
+
+			it(
+				'should trim pasted cells\' height if they exceeds pasted table height (pasted width is bigger then selection width)',
+				() => {
+					// Select 00 -> 11 (selection 2x2 - smaller by width than the expected fixed table)
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+					);
+
+					// +----+----+----+
+					// | aa | ab | ac |
+					// +----+----+    +
+					// | ba | bb |    |   <- Last row establish table height=2.
+					// +----+    +    +
+					//      |    |    |   <- Cell "ac" sticks out by 1 slot.
+					//      +    +----+
+					//      |    |        <- Cell "bb" sticks out by 2 slots.
+					//      +----+
+					pasteTable( [
+						[ 'aa', 'ab', { contents: 'ac', rowspan: 3 } ],
+						[ 'ba', { contents: 'bb', rowspan: 3 } ]
+					] );
+
+					// +----+----+----+----+
+					// | aa | ab | 02 | 03 |
+					// +----+----+----+----+
+					// | ba | bb | 12 | 13 |
+					// +----+----+----+----+
+					// | 20 | 21 | 22 | 23 |
+					// +----+----+----+----+
+					// | 30 | 31 | 32 | 33 |
+					// +----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02', '03' ],
+						[ 'ba', 'bb', '12', '13' ],
+						[ '20', '21', '22', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					/* eslint-disable no-multi-spaces */
+					assertSelectedCells( model, [
+						[ 1, 1, 0, 0 ],
+						[ 1, 1, 0, 0 ],
+						[ 0, 0, 0, 0 ],
+						[ 0, 0, 0, 0 ]
+					] );
+					/* eslint-enable no-multi-spaces */
+				}
+			);
+
+			it(
+				`should trim pasted pasted cells' height and width if they exceeds pasted table dimensions
+				(pasted table is bigger then selection width)`,
+				() => {
+					// Select 00 -> 11 (selection 2x2 - smaller than the expected fixed table)
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+					);
+
+					// +----+----+----+
+					// | aa | ab | ac |
+					// +----+----+----+----+
+					// | ba | bb           | <- Cell "bb" sticks out by 1 slots in width and by 1 slot in height.
+					// +----+              +
+					// | ca |              |
+					// +----+              +
+					//      |              |
+					//      +----+----+----+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', { contents: 'bb', colspan: 3, rowspan: 2 } ]
+					] );
+
+					// +----+----+----+----+
+					// | aa | ab | 02 | 03 |
+					// +----+----+----+----+
+					// | ba | bb | 12 | 13 |
+					// +----+----+----+----+
+					// | 20 | 21 | 22 | 23 |
+					// +----+----+----+----+
+					// | 30 | 31 | 32 | 33 |
+					// +----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02', '03' ],
+						[ 'ba', 'bb', '12', '13' ],
+						[ '20', '21', '22', '23' ],
+						[ '30', '31', '32', '33' ]
+					] ) );
+
+					/* eslint-disable no-multi-spaces */
+					assertSelectedCells( model, [
+						[ 1, 1, 0, 0 ],
+						[ 1, 1, 0, 0 ],
+						[ 0, 0, 0, 0 ],
+						[ 0, 0, 0, 0 ]
+					] );
+					/* eslint-enable no-multi-spaces */
+				}
+			);
+		} );
 	} );
 
 	describe( 'Clipboard integration - paste (content scenarios)', () => {

+ 89 - 1
packages/ckeditor5-table/tests/utils.js

@@ -13,7 +13,7 @@ import { modelTable } from './_utils/utils';
 import {
 	getSelectedTableCells,
 	getTableCellsContainingSelection,
-	getSelectionAffectedTableCells
+	getSelectionAffectedTableCells, getVerticallyOverlappingCells, getHorizontallyOverlappingCells
 } from '../src/utils';
 
 describe( 'table utils', () => {
@@ -359,4 +359,92 @@ describe( 'table utils', () => {
 			expect( getSelectionAffectedTableCells( selection ) ).to.be.empty;
 		} );
 	} );
+
+	describe( 'getVerticallyOverlappingCells()', () => {
+		let table;
+
+		beforeEach( () => {
+			// +----+----+----+----+----+
+			// | 00 | 01 | 02 | 03 | 04 |
+			// +    +    +----+    +----+
+			// |    |    | 12 |    | 14 |
+			// +    +    +    +----+----+
+			// |    |    |    | 23 | 24 |
+			// +    +----+    +    +----+
+			// |    | 31 |    |    | 34 |
+			// +    +    +----+----+----+
+			// |    |    | 42 | 43 | 44 |
+			// +----+----+----+----+----+
+			setModelData( model, modelTable( [
+				[ { contents: '00', rowspan: 5 }, { contents: '01', rowspan: 3 }, '02', { contents: '03', rowspan: 2 }, '04' ],
+				[ { contents: '12', rowspan: 3 }, '14' ],
+				[ { contents: '23', rowspan: 2 }, '24' ],
+				[ { contents: '31', rowspan: 2 }, '34' ],
+				[ '42', '43', '44' ]
+			] ) );
+
+			table = modelRoot.getChild( 0 );
+		} );
+
+		it( 'should return empty array for no overlapping cells', () => {
+			const cellsInfo = getVerticallyOverlappingCells( table, 0 );
+
+			expect( cellsInfo ).to.be.empty;
+		} );
+
+		it( 'should return overlapping cells info for given overlapRow', () => {
+			const cellsInfo = getVerticallyOverlappingCells( table, 2 );
+
+			expect( cellsInfo[ 0 ].cell ).to.equal( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); // Cell 00
+			expect( cellsInfo[ 1 ].cell ).to.equal( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) ); // Cell 01
+			expect( cellsInfo[ 2 ].cell ).to.equal( modelRoot.getNodeByPath( [ 0, 1, 0 ] ) ); // Cell 12
+		} );
+
+		it( 'should ignore rows below startRow', () => {
+			const cellsInfo = getVerticallyOverlappingCells( table, 2, 1 );
+
+			expect( cellsInfo[ 0 ].cell ).to.equal( modelRoot.getNodeByPath( [ 0, 1, 0 ] ) ); // Cell 12
+		} );
+	} );
+
+	describe( 'getHorizontallyOverlappingCells()', () => {
+		let table;
+
+		beforeEach( () => {
+			// +----+----+----+----+----+
+			// | 00                     |
+			// +----+----+----+----+----+
+			// | 10           | 13      |
+			// +----+----+----+----+----+
+			// | 20 | 21           | 24 |
+			// +----+----+----+----+----+
+			// | 30      | 32      | 34 |
+			// +----+----+----+----+----+
+			// | 40 | 41 | 42 | 43 | 44 |
+			// +----+----+----+----+----+
+			setModelData( model, modelTable( [
+				[ { contents: '00', colspan: 5 } ],
+				[ { contents: '10', colspan: 3 }, { contents: '13', colspan: 2 } ],
+				[ '20', { contents: '21', colspan: 3 }, '24' ],
+				[ { contents: '30', colspan: 2 }, { contents: '32', colspan: 2 }, '34' ],
+				[ '40', '41', '42', '43', '44' ]
+			] ) );
+
+			table = modelRoot.getChild( 0 );
+		} );
+
+		it( 'should return empty array for no overlapping cells', () => {
+			const cellsInfo = getHorizontallyOverlappingCells( table, 0 );
+
+			expect( cellsInfo ).to.be.empty;
+		} );
+
+		it( 'should return overlapping cells info for given overlapColumn', () => {
+			const cellsInfo = getHorizontallyOverlappingCells( table, 2 );
+
+			expect( cellsInfo[ 0 ].cell ).to.equal( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) ); // Cell 00
+			expect( cellsInfo[ 1 ].cell ).to.equal( modelRoot.getNodeByPath( [ 0, 1, 0 ] ) ); // Cell 10
+			expect( cellsInfo[ 2 ].cell ).to.equal( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) ); // Cell 21
+		} );
+	} );
 } );