8
0
Просмотр исходного кода

Merge branch 'master' into i/7227

Kuba Niegowski 5 лет назад
Родитель
Сommit
726c000dba

+ 153 - 103
packages/ckeditor5-table/src/tableclipboard.js

@@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import TableSelection from './tableselection';
 import TableWalker from './tablewalker';
-import { getColumnIndexes, getRowIndexes, isSelectionRectangular } from './utils';
+import { getColumnIndexes, getRowIndexes, getSelectionAffectedTableCells, isSelectionRectangular } from './utils';
 import { findAncestor } from './commands/utils';
 import { cropTableToDimensions } from './tableselection/croptable';
 import TableUtils from './tableutils';
@@ -101,13 +101,18 @@ export default class TableClipboard extends Plugin {
 			return;
 		}
 
-		const tableSelection = this.editor.plugins.get( TableSelection );
-		const selectedTableCells = tableSelection.getSelectedTableCells();
+		const model = this.editor.model;
+		const tableUtils = this.editor.plugins.get( TableUtils );
+
+		const selectedTableCells = getSelectionAffectedTableCells( model.document.selection );
 
-		if ( !selectedTableCells ) {
+		if ( !selectedTableCells.length ) {
 			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 );
 
@@ -118,43 +123,39 @@ export default class TableClipboard extends Plugin {
 		// Override default model.insertContent() handling at this point.
 		evt.stop();
 
-		// Currently not handled. See: https://github.com/ckeditor/ckeditor5/issues/6121.
-		if ( selectedTableCells.length === 1 ) {
-			// @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Single table cell is selected.' );
-
-			return;
-		}
-
-		const tableUtils = this.editor.plugins.get( TableUtils );
+		const pasteWidth = tableUtils.getColumns( pastedTable );
+		const pasteHeight = tableUtils.getRows( pastedTable );
 
-		// Currently not handled. The selected table content should be trimmed to a rectangular selection.
-		// See: https://github.com/ckeditor/ckeditor5/issues/6122.
-		if ( !isSelectionRectangular( selectedTableCells, tableUtils ) ) {
-			// @if CK_DEBUG // console.log( 'NOT IMPLEMENTED YET: Selection is not rectangular (non-mergeable).' );
+		model.change( writer => {
+			let { first: firstColumnOfSelection, last: lastColumnOfSelection } = getColumnIndexes( selectedTableCells );
+			let { first: firstRowOfSelection, last: lastRowOfSelection } = getRowIndexes( selectedTableCells );
 
-			return;
-		}
+			if ( selectedTableCells.length == 1 ) {
+				lastRowOfSelection += pasteHeight - 1;
+				lastColumnOfSelection += pasteWidth - 1;
 
-		const { last: lastColumnOfSelection, first: firstColumnOfSelection } = getColumnIndexes( selectedTableCells );
-		const { first: firstRowOfSelection, last: lastRowOfSelection } = getRowIndexes( selectedTableCells );
+				expandTableSize( selectedTable, lastRowOfSelection + 1, lastColumnOfSelection + 1, writer, tableUtils );
+			}
 
-		const selectionHeight = lastRowOfSelection - firstRowOfSelection + 1;
-		const selectionWidth = lastColumnOfSelection - firstColumnOfSelection + 1;
+			// 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).' );
 
-		const pasteHeight = tableUtils.getRows( pastedTable );
-		const pasteWidth = tableUtils.getColumns( pastedTable );
+				return;
+			}
 
-		// The if below is temporal and will be removed when handling this case.
-		// 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.' );
+			const selectionHeight = lastRowOfSelection - firstRowOfSelection + 1;
+			const selectionWidth = lastColumnOfSelection - firstColumnOfSelection + 1;
 
-			return;
-		}
+			// The if below is temporal and will be removed when handling this case.
+			// 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.' );
 
-		const model = this.editor.model;
+				return;
+			}
 
-		model.change( writer => {
 			// Crop pasted table if it extends selection area.
 			if ( selectionHeight < pasteHeight || selectionWidth < pasteWidth ) {
 				const cropDimensions = {
@@ -167,79 +168,128 @@ export default class TableClipboard extends Plugin {
 				pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer, tableUtils );
 			}
 
-			// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
-			const pastedTableLocationMap = createLocationMap( pastedTable, selectionWidth, selectionHeight );
-
-			// Content table to which we insert a pasted table.
-			const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] );
-
-			const selectedTableMap = [ ...new TableWalker( selectedTable, {
-				startRow: firstRowOfSelection,
-				endRow: lastRowOfSelection,
-				includeSpanned: true
-			} ) ];
-
-			// Selection must be set to pasted cells (some might be removed or new created).
-			const cellsToSelect = [];
-
-			// Store previous cell in order to insert a new table cells after it (if required).
-			let previousCellInRow;
-
-			// Content table replace cells algorithm iterates over a selected table fragment and:
-			//
-			// - Removes existing table cells at current slot (location).
-			// - Inserts cell from a pasted table for a matched slots.
-			//
-			// This ensures proper table geometry after the paste
-			for ( const { row, column, cell, isSpanned } of selectedTableMap ) {
-				if ( column === 0 ) {
-					previousCellInRow = null;
-				}
-
-				// Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785.
-				if ( column < firstColumnOfSelection || column > lastColumnOfSelection ) {
-					// Only update the previousCellInRow for non-spanned slots.
-					if ( !isSpanned ) {
-						previousCellInRow = cell;
-					}
-
-					continue;
-				}
-
-				// If the slot is occupied by a cell in a selected table - remove it.
-				// The slot of this cell will be either:
-				// - Replaced by a pasted table cell.
-				// - Spanned by a previously pasted table cell.
-				if ( !isSpanned ) {
-					writer.remove( cell );
-				}
-
-				// Map current table slot location to an pasted table slot location.
-				const pastedCell = pastedTableLocationMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ];
-
-				// There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
-				if ( !pastedCell ) {
-					continue;
-				}
-
-				// Clone cell to insert (to duplicate its attributes and children).
-				// Cloning is required to support repeating pasted table content when inserting to a bigger selection.
-				const cellToInsert = pastedCell._clone( true );
-
-				let insertPosition;
-
-				if ( !previousCellInRow ) {
-					insertPosition = writer.createPositionAt( selectedTable.getChild( row ), 0 );
-				} else {
-					insertPosition = writer.createPositionAfter( previousCellInRow );
-				}
-
-				writer.insert( cellToInsert, insertPosition );
-				cellsToSelect.push( cellToInsert );
-				previousCellInRow = cellToInsert;
+			const selectionDimensions = {
+				firstColumnOfSelection,
+				firstRowOfSelection,
+				lastColumnOfSelection,
+				lastRowOfSelection,
+				selectionHeight,
+				selectionWidth
+			};
+
+			replaceSelectedCellsWithPasted( pastedTable, selectedTable, selectionDimensions, writer );
+		} );
+	}
+}
+
+// Replaces the part of selectedTable with pastedTable.
+//
+// @param {module:engine/model/element~Element} pastedTable
+// @param {module:engine/model/element~Element} selectedTable
+// @param {Object} selectionDimensions
+// @param {Number} selectionDimensions.firstColumnOfSelection
+// @param {Number} selectionDimensions.firstRowOfSelection
+// @param {Number} selectionDimensions.lastColumnOfSelection
+// @param {Number} selectionDimensions.lastRowOfSelection
+// @param {Number} selectionDimensions.selectionHeight
+// @param {Number} selectionDimensions.selectionWidth
+// @param {module:engine/model/writer~Writer} writer
+function replaceSelectedCellsWithPasted( pastedTable, selectedTable, selectionDimensions, writer ) {
+	const {
+		firstColumnOfSelection, lastColumnOfSelection, selectionWidth,
+		firstRowOfSelection, lastRowOfSelection, selectionHeight
+	} = selectionDimensions;
+
+	// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
+	const pastedTableLocationMap = createLocationMap( pastedTable, selectionWidth, selectionHeight );
+
+	const selectedTableMap = [ ...new TableWalker( selectedTable, {
+		startRow: firstRowOfSelection,
+		endRow: lastRowOfSelection,
+		includeSpanned: true
+	} ) ];
+
+	// Selection must be set to pasted cells (some might be removed or new created).
+	const cellsToSelect = [];
+
+	// Store previous cell in order to insert a new table cells after it (if required).
+	let previousCellInRow;
+
+	// Content table replace cells algorithm iterates over a selected table fragment and:
+	//
+	// - Removes existing table cells at current slot (location).
+	// - Inserts cell from a pasted table for a matched slots.
+	//
+	// This ensures proper table geometry after the paste
+	for ( const { row, column, cell, isSpanned } of selectedTableMap ) {
+		if ( column === 0 ) {
+			previousCellInRow = null;
+		}
+
+		// Could use startColumn, endColumn. See: https://github.com/ckeditor/ckeditor5/issues/6785.
+		if ( column < firstColumnOfSelection || column > lastColumnOfSelection ) {
+			// Only update the previousCellInRow for non-spanned slots.
+			if ( !isSpanned ) {
+				previousCellInRow = cell;
 			}
 
-			writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
+			continue;
+		}
+
+		// If the slot is occupied by a cell in a selected table - remove it.
+		// The slot of this cell will be either:
+		// - Replaced by a pasted table cell.
+		// - Spanned by a previously pasted table cell.
+		if ( !isSpanned ) {
+			writer.remove( cell );
+		}
+
+		// Map current table slot location to an pasted table slot location.
+		const pastedCell = pastedTableLocationMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ];
+
+		// There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
+		if ( !pastedCell ) {
+			continue;
+		}
+
+		// Clone cell to insert (to duplicate its attributes and children).
+		// Cloning is required to support repeating pasted table content when inserting to a bigger selection.
+		const cellToInsert = pastedCell._clone( true );
+
+		let insertPosition;
+
+		if ( !previousCellInRow ) {
+			insertPosition = writer.createPositionAt( selectedTable.getChild( row ), 0 );
+		} else {
+			insertPosition = writer.createPositionAfter( previousCellInRow );
+		}
+
+		writer.insert( cellToInsert, insertPosition );
+		cellsToSelect.push( cellToInsert );
+		previousCellInRow = cellToInsert;
+	}
+
+	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 ) {
+	const tableWidth = tableUtils.getColumns( table );
+	const tableHeight = tableUtils.getRows( table );
+
+	if ( columns > tableWidth ) {
+		tableUtils.insertColumns( table, {
+			batch: writer.batch,
+			at: tableWidth,
+			columns: columns - tableWidth
+		} );
+	}
+
+	if ( rows > tableHeight ) {
+		tableUtils.insertRows( table, {
+			batch: writer.batch,
+			at: tableHeight,
+			rows: rows - tableHeight
 		} );
 	}
 }

+ 520 - 17
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -124,6 +124,30 @@ describe( 'table clipboard', () => {
 			] ) );
 		} );
 
+		it( 'should not alter model.insertContent if selection is outside table', () => {
+			model.change( writer => {
+				writer.insertElement( 'paragraph', modelRoot.getChild( 0 ), 'before' );
+				writer.setSelection( modelRoot.getChild( 0 ), 'before' );
+			} );
+
+			const data = {
+				dataTransfer: createDataTransfer(),
+				preventDefault: sinon.spy(),
+				stopPropagation: sinon.spy()
+			};
+			data.dataTransfer.setData( 'text/html', '<p>foo</p>' );
+			viewDocument.fire( 'paste', data );
+
+			editor.isReadOnly = false;
+
+			assertEqualMarkup( getModelData( model ), '<paragraph>foo[]</paragraph>' + modelTable( [
+				[ '00', '01', '02', '03' ],
+				[ '10', '11', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+		} );
+
 		it( 'should not alter model.insertContent if no table pasted', () => {
 			tableSelection.setCellSelection(
 				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
@@ -276,31 +300,498 @@ describe( 'table clipboard', () => {
 		} );
 
 		describe( 'single cell selected', () => {
-			it( 'blocks this case', () => {
+			beforeEach( () => {
 				setModelData( model, modelTable( [
-					[ '00', '01', '02' ],
+					[ '00[]', '01', '02' ],
 					[ '10', '11', '12' ],
 					[ '20', '21', '22' ]
 				] ) );
+			} );
 
-				tableSelection.setCellSelection(
-					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
-					modelRoot.getNodeByPath( [ 0, 0, 0 ] )
-				);
+			describe( 'with the selection on the middle cell', () => {
+				beforeEach( () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+					);
+				} );
 
-				// Catches the temporary console log in the CK_DEBUG mode.
-				sinon.stub( console, 'log' );
+				it( 'should replace the table cells (with single undo step)', () => {
+					const batches = setupBatchWatch();
 
-				pasteTable( [
-					[ 'aa', 'ab' ],
-					[ 'ba', 'bb' ]
-				] );
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
 
-				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
-					[ '00', '01', '02' ],
-					[ '10', '11', '12' ],
-					[ '20', '21', '22' ]
-				] ) );
+					expect( batches.size ).to.equal( 1 );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02' ],
+						[ '10', 'aa', 'ab' ],
+						[ '20', 'ba', 'bb' ]
+					] ) );
+				} );
+
+				it( 'should expand the table width and replace the table cells (with single undo step)', () => {
+					const batches = setupBatchWatch();
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ]
+					] );
+
+					expect( batches.size ).to.equal( 1 );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '', '', '' ],
+						[ '10', 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ '20', 'ba', 'bb', 'bc', 'bd', 'be' ]
+					] ) );
+				} );
+
+				it( 'should expand the table height and replace the table cells (with single undo step)', () => {
+					const batches = setupBatchWatch();
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ],
+						[ 'ca', 'cb' ],
+						[ 'da', 'db' ],
+						[ 'ea', 'eb' ]
+					] );
+
+					expect( batches.size ).to.equal( 1 );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02' ],
+						[ '10', 'aa', 'ab' ],
+						[ '20', 'ba', 'bb' ],
+						[ '', 'ca', 'cb' ],
+						[ '', 'da', 'db' ],
+						[ '', 'ea', 'eb' ]
+					] ) );
+				} );
+
+				it( 'should expand the table width, height and replace the table cells (with single undo step)', () => {
+					const batches = setupBatchWatch();
+
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] );
+
+					expect( batches.size ).to.equal( 1 );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '', '', '' ],
+						[ '10', 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ '20', 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ '', 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ '', 'da', 'db', 'dc', 'dd', 'de' ],
+						[ '', 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'with the selection on the first cell', () => {
+				beforeEach( () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 0, 0 ] )
+					);
+				} );
+
+				it( 'should replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02' ],
+						[ 'ba', 'bb', '12' ],
+						[ '20', '21', '22' ]
+					] ) );
+				} );
+
+				it( 'should expand the table and replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'with the selection on the middle cell of the first row', () => {
+				beforeEach( () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+						modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+					);
+				} );
+
+				it( 'should replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', 'aa', 'ab' ],
+						[ '10', 'ba', 'bb' ],
+						[ '20', '21', '22' ]
+					] ) );
+				} );
+
+				it( 'should expand the table and replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ '10', 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ '20', 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ '', 'da', 'db', 'dc', 'dd', 'de' ],
+						[ '', 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'with the selection on the last cell of the first row', () => {
+				beforeEach( () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
+						modelRoot.getNodeByPath( [ 0, 0, 2 ] )
+					);
+				} );
+
+				it( 'should replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', 'aa', 'ab' ],
+						[ '10', '11', 'ba', 'bb' ],
+						[ '20', '21', '22', '' ]
+					] ) );
+				} );
+
+				it( 'should expand the table and replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ '10', '11', 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ '20', '21', 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ '', '', 'da', 'db', 'dc', 'dd', 'de' ],
+						[ '', '', 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'with the selection is on the middle cell of the first column', () => {
+				beforeEach( () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 1, 0 ] )
+					);
+				} );
+
+				it( 'should replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02' ],
+						[ 'aa', 'ab', '12' ],
+						[ 'ba', 'bb', '22' ]
+					] ) );
+				} );
+
+				it( 'should expand the table and replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '', '' ],
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'with the selection is on the last cell of the first column', () => {
+				beforeEach( () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 0 ] )
+					);
+				} );
+
+				it( 'should replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02' ],
+						[ '10', '11', '12' ],
+						[ 'aa', 'ab', '22' ],
+						[ 'ba', 'bb', '' ]
+					] ) );
+				} );
+
+				it( 'should expand the table and replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '', '' ],
+						[ '10', '11', '12', '', '' ],
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'with the selection is on the last cell of the last column', () => {
+				beforeEach( () => {
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 2, 2 ] ),
+						modelRoot.getNodeByPath( [ 0, 2, 2 ] )
+					);
+				} );
+
+				it( 'should replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '' ],
+						[ '10', '11', '12', '' ],
+						[ '20', '21', 'aa', 'ab' ],
+						[ '', '', 'ba', 'bb' ]
+					] ) );
+				} );
+
+				it( 'should expand the table and replace the table cells', () => {
+					pasteTable( [
+						[ 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ 'da', 'db', 'dc', 'dd', 'de' ],
+						[ 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '', '', '', '' ],
+						[ '10', '11', '12', '', '', '', '' ],
+						[ '20', '21', 'aa', 'ab', 'ac', 'ad', 'ae' ],
+						[ '', '', 'ba', 'bb', 'bc', 'bd', 'be' ],
+						[ '', '', 'ca', 'cb', 'cc', 'cd', 'ce' ],
+						[ '', '', 'da', 'db', 'dc', 'dd', 'de' ],
+						[ '', '', 'ea', 'eb', 'ec', 'ed', 'ee' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'the selection inside the table cell', () => {
+				it( 'should replace the table cells when the collapsed selection is in the first cell', () => {
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02' ],
+						[ 'ba', 'bb', '12' ],
+						[ '20', '21', '22' ]
+					] ) );
+				} );
+
+				it( 'should replace the table cells when the expanded selection is in the first cell', () => {
+					model.change( writer => {
+						writer.setSelection( modelRoot.getNodeByPath( [ 0, 0, 0 ] ), 'in' );
+					} );
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ 'aa', 'ab', '02' ],
+						[ 'ba', 'bb', '12' ],
+						[ '20', '21', '22' ]
+					] ) );
+				} );
+
+				it( 'should replace the table cells when selection is on the image inside the table cell', async () => {
+					await editor.destroy();
+					await createEditor( [ ImageEditing, ImageCaptionEditing ] );
+
+					setModelData( model, modelTable( [
+						[ '00', '01', '02' ],
+						[ '10', '11', '12' ],
+						[ '20', '21', '[<image src="/assets/sample.jpg"><caption></caption></image>]' ]
+					] ) );
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '' ],
+						[ '10', '11', '12', '' ],
+						[ '20', '21', 'aa', 'ab' ],
+						[ '', '', 'ba', 'bb' ]
+					] ) );
+				} );
+
+				it( 'should replace the table cells when selection is in the image caption inside the table cell', async () => {
+					await editor.destroy();
+					await createEditor( [ ImageEditing, ImageCaptionEditing ] );
+
+					setModelData( model, modelTable( [
+						[ '00', '01', '02' ],
+						[ '10', '11', '12' ],
+						[ '20', '21', '<image src="/assets/sample.jpg"><caption>fo[]o</caption></image>' ]
+					] ) );
+
+					pasteTable( [
+						[ 'aa', 'ab' ],
+						[ 'ba', 'bb' ]
+					] );
+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', '02', '' ],
+						[ '10', '11', '12', '' ],
+						[ '20', '21', 'aa', 'ab' ],
+						[ '', '', 'ba', 'bb' ]
+					] ) );
+				} );
+			} );
+
+			describe( 'with spanned cells', () => {
+				it( 'should replace the table structure', () => {
+					// +----+----+----+----+----+----+
+					// | 00 | 01 | 02           | 05 |
+					// +----+----+----+----+----+----+
+					// | 10 | 11      | 13 | 14 | 15 |
+					// +----+         +----+----+----+
+					// | 20 |         | 23      | 25 |
+					// +----+         +----+----+    +
+					// | 30 |         | 33      |    |
+					// +----+----+----+----+----+----+
+					// | 40      | 42 | 43 | 44      |
+					// +----+----+----+----+----+----+
+					setModelData( model, modelTable( [
+						[ '00', '01', { contents: '02', colspan: 3 }, '05' ],
+						[ '10', { contents: '11', colspan: 2, rowspan: 3 }, '13', '14', '15' ],
+						[ '20', { contents: '23', colspan: 2 }, { contents: '25', rowspan: 2 } ],
+						[ '30', { contents: '33', colspan: 2 } ],
+						[ { contents: '40', colspan: 2 }, '42', '43', { contents: '44', colspan: 2 } ]
+					] ) );
+
+					tableSelection.setCellSelection(
+						modelRoot.getNodeByPath( [ 0, 2, 1 ] ), // Cell 23.
+						modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+					);
+
+					// +----+----+----+----+----+
+					// | aa      | ac | ad | ae |
+					// +         +    +----+----+
+					// |         |    | bd | be |
+					// +----+----+----+----+----+
+					// | ca      | cc | cd | ce |
+					// +----+----+----+----+----+
+					// | da           | dd | de |
+					// +----+----+----+----+    +
+					// | ea | eb | ec | ed |    |
+					// +----+----+----+----+----+
+					pasteTable( [
+						[ { contents: 'aa', colspan: 2, rowspan: 2 }, { contents: 'ac', rowspan: 2 }, 'ad', 'ae' ],
+						[ 'bd', 'be' ],
+						[ { contents: 'ca', colspan: 2 }, 'cc', 'cd', 'ce' ],
+						[ { contents: 'da', colspan: 3 }, 'dd', { contents: 'de', rowspan: 2 } ],
+						[ 'ea', 'eb', 'ec', 'ed' ]
+					] );
+
+					// +----+----+----+----+----+----+----+----+
+					// | 00 | 01 | 02           | 05 |    |    |
+					// +----+----+----+----+----+----+----+----+
+					// | 10 | 11      | 13 | 14 | 15 |    |    |
+					// +----+         +----+----+----+----+----+
+					// | 20 |         | aa      | ac | ad | ae |
+					// +----+         +         +    +----+----+
+					// | 30 |         |         |    | bd | be |
+					// +----+----+----+----+----+----+----+----+
+					// | 40      | 42 | ca      | cc | cd | ce |
+					// +----+----+----+----+----+----+----+----+
+					// |    |    |    | da           | dd | de |
+					// +----+----+----+----+----+----+----+    +
+					// |    |    |    | ea | eb | ec | ed |    |
+					// +----+----+----+----+----+----+----+----+
+					assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+						[ '00', '01', { contents: '02', colspan: 3 }, '05', '', '' ],
+						[ '10', { contents: '11', colspan: 2, rowspan: 3 }, '13', '14', '15', '', '' ],
+						[ '20', { contents: 'aa', colspan: 2, rowspan: 2 }, { contents: 'ac', rowspan: 2 }, 'ad', 'ae' ],
+						[ '30', 'bd', 'be' ],
+						[ { contents: '40', colspan: 2 }, '42', { contents: 'ca', colspan: 2 }, 'cc', 'cd', 'ce' ],
+						[ '', '', '', { contents: 'da', colspan: 3 }, 'dd', { contents: 'de', rowspan: 2 } ],
+						[ '', '', '', 'ea', 'eb', 'ec', 'ed' ]
+					] ) );
+				} );
 			} );
 		} );
 
@@ -1712,4 +2203,16 @@ describe( 'table clipboard', () => {
 			}
 		};
 	}
+
+	function setupBatchWatch() {
+		const createdBatches = new Set();
+
+		model.on( 'applyOperation', ( evt, [ operation ] ) => {
+			if ( operation.isDocumentOperation ) {
+				createdBatches.add( operation.batch );
+			}
+		} );
+
+		return createdBatches;
+	}
 } );

+ 0 - 1
packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css

@@ -18,7 +18,6 @@
 	 * Styles of the type around buttons
 	 */
 	& .ck-widget__type-around__button {
-		cursor: pointer;
 		width: var(--ck-widget-type-around-button-size);
 		height: var(--ck-widget-type-around-button-size);
 		background: var(--ck-color-widget-type-around-button);