Explorar el Código

Added support for pasting table when only one table cell is selected.

Kuba Niegowski hace 5 años
padre
commit
f8e2635ab7

+ 151 - 93
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,10 +101,12 @@ 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 );
 
-		if ( !selectedTableCells ) {
+		const selectedTableCells = getSelectionAffectedTableCells( model.document.selection );
+
+		if ( !selectedTableCells.length ) {
 			return;
 		}
 
@@ -118,43 +120,28 @@ 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 );
-
-		// 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).' );
-
-			return;
-		}
-
-		const { last: lastColumnOfSelection, first: firstColumnOfSelection } = getColumnIndexes( selectedTableCells );
-		const { first: firstRowOfSelection, last: lastRowOfSelection } = getRowIndexes( selectedTableCells );
-
-		const selectionHeight = lastRowOfSelection - firstRowOfSelection + 1;
-		const selectionWidth = lastColumnOfSelection - firstColumnOfSelection + 1;
-
-		const pasteHeight = tableUtils.getRows( pastedTable );
 		const pasteWidth = tableUtils.getColumns( pastedTable );
+		const pasteHeight = tableUtils.getRows( pastedTable );
 
-		// 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.' );
+		model.change( writer => {
+			const selectionDimensions = this._prepareTableSelectionForPasting( selectedTableCells, pasteWidth, pasteHeight, writer );
+			const { selectionWidth, selectionHeight } = selectionDimensions;
+
+			// This paste scenario is not handled for now, this will be removed.
+			// See: https://github.com/ckeditor/ckeditor5/issues/6122.
+			// TODO temporary to remove after https://github.com/ckeditor/ckeditor5/issues/6122.
+			if ( selectionDimensions.stop ) {
+				return;
+			}
 
-			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,83 +154,154 @@ 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;
-				}
+			this._replaceSelectedCellsWithPasted( selectedTable, pastedTable, selectionDimensions, writer );
+		} );
+	}
 
-				// 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;
-					}
+	_prepareTableSelectionForPasting( selectedTableCells, pasteWidth, pasteHeight, writer ) {
+		const tableUtils = this.editor.plugins.get( TableUtils );
+		const selectedTable = findAncestor( 'table', selectedTableCells[ 0 ] );
 
-					continue;
-				}
+		let { first: firstColumnOfSelection, last: lastColumnOfSelection } = getColumnIndexes( selectedTableCells );
+		let { first: firstRowOfSelection, last: lastRowOfSelection } = getRowIndexes( selectedTableCells );
+
+		if ( selectedTableCells.length == 1 ) {
+			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).' );
 
-				// 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.
+			// TODO this is temporary - https://github.com/ckeditor/ckeditor5/issues/6122
+			prepareLandingPlace();
+
+			// TODO this is temporary stop marker for currently unhandled scenario
+			return { stop: true };
+		}
+
+		return {
+			firstColumnOfSelection,
+			firstRowOfSelection,
+			lastColumnOfSelection,
+			lastRowOfSelection,
+			selectionHeight: lastRowOfSelection - firstRowOfSelection + 1,
+			selectionWidth: lastColumnOfSelection - firstColumnOfSelection + 1
+		};
+	}
+
+	_replaceSelectedCellsWithPasted( selectedTable, pastedTable, 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 ) {
-					writer.remove( cell );
+					previousCellInRow = cell;
 				}
 
-				// Map current table slot location to an pasted table slot location.
-				const pastedCell = pastedTableLocationMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ];
+				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 );
+			}
 
-				// 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;
-				}
+			// Map current table slot location to an pasted table slot location.
+			const pastedCell = pastedTableLocationMap[ row - firstRowOfSelection ][ column - firstColumnOfSelection ];
 
-				// 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 );
+			// 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;
+			}
 
-				let insertPosition;
+			// 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 );
 
-				if ( !previousCellInRow ) {
-					insertPosition = writer.createPositionAt( selectedTable.getChild( row ), 0 );
-				} else {
-					insertPosition = writer.createPositionAfter( previousCellInRow );
-				}
+			let insertPosition;
 
-				writer.insert( cellToInsert, insertPosition );
-				cellsToSelect.push( cellToInsert );
-				previousCellInRow = cellToInsert;
+			if ( !previousCellInRow ) {
+				insertPosition = writer.createPositionAt( selectedTable.getChild( row ), 0 );
+			} else {
+				insertPosition = writer.createPositionAfter( previousCellInRow );
 			}
 
-			writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
+			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
 		} );
 	}
 }
 
+// TODO rename to reflect that it's splitting cells to make selection rectangular
+function prepareLandingPlace( ) {
+}
+
 function getTableIfOnlyTableInContent( content ) {
 	// Table passed directly.
 	if ( content.is( 'table' ) ) {

+ 133 - 5
packages/ckeditor5-table/tests/tableclipboard-paste.js

@@ -124,6 +124,30 @@ describe( 'table clipboard', () => {
 			] ) );
 		} );
 
+		it( 'should not alter model.insertContent if no table cells are selected', () => {
+			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,20 +300,104 @@ 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' ]
 				] ) );
+			} );
 
+			it( 'with selection on the first cell', () => {
 				tableSelection.setCellSelection(
 					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
 					modelRoot.getNodeByPath( [ 0, 0, 0 ] )
 				);
 
-				// 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( [
+					[ 'aa', 'ab', '02' ],
+					[ 'ba', 'bb', '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+			} );
+
+			it( 'with selection 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( 'with selection on the middle cell of the first row', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+				);
+
+				pasteTable( [
+					[ 'aa', 'ab' ],
+					[ 'ba', 'bb' ]
+				] );
+
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ '00', 'aa', 'ab' ],
+					[ '10', 'ba', 'bb' ],
+					[ '20', '21', '22' ]
+				] ) );
+			} );
+
+			it( 'with selection on the last cell of the first row', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 2 ] )
+				);
+
+				pasteTable( [
+					[ 'aa', 'ab' ],
+					[ 'ba', 'bb' ]
+				] );
+
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ '00', '01', 'aa', 'ab' ],
+					[ '10', '11', 'ba', 'bb' ],
+					[ '20', '21', '22', '' ]
+				] ) );
+			} );
+
+			it( 'with selection on the middle cell of the first column', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] )
+				);
+
+				pasteTable( [
+					[ 'aa', 'ab' ],
+					[ 'ba', 'bb' ]
+				] );
+
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ '00', '01', '02' ],
+					[ 'aa', 'ab', '12' ],
+					[ 'ba', 'bb', '22' ]
+				] ) );
+			} );
+
+			it( 'with selection on the last cell of the first column', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 0 ] )
+				);
 
 				pasteTable( [
 					[ 'aa', 'ab' ],
@@ -299,7 +407,27 @@ describe( 'table clipboard', () => {
 				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
 					[ '00', '01', '02' ],
 					[ '10', '11', '12' ],
-					[ '20', '21', '22' ]
+					[ 'aa', 'ab', '22' ],
+					[ 'ba', 'bb', '' ]
+				] ) );
+			} );
+
+			it( 'with selection on the last cell of the last column', () => {
+				tableSelection.setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 2, 2 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 2 ] )
+				);
+
+				pasteTable( [
+					[ 'aa', 'ab' ],
+					[ 'ba', 'bb' ]
+				] );
+
+				assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+					[ '00', '01', '02', '' ],
+					[ '10', '11', '12', '' ],
+					[ '20', '21', 'aa', 'ab' ],
+					[ '', '', 'ba', 'bb' ]
 				] ) );
 			} );
 		} );