Răsfoiți Sursa

Merge branch 'master' into i/6107

panr 5 ani în urmă
părinte
comite
80b83f0529
36 a modificat fișierele cu 2020 adăugiri și 1292 ștergeri
  1. 6 5
      packages/ckeditor5-table/src/commands/insertcolumncommand.js
  2. 7 4
      packages/ckeditor5-table/src/commands/insertrowcommand.js
  3. 13 0
      packages/ckeditor5-table/src/commands/utils.js
  4. 9 3
      packages/ckeditor5-table/src/table.js
  5. 31 7
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js
  6. 3 4
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js
  7. 6 29
      packages/ckeditor5-table/src/tableclipboard.js
  8. 2 3
      packages/ckeditor5-table/src/tableproperties/tablepropertiesediting.js
  9. 305 167
      packages/ckeditor5-table/src/tableselection.js
  10. 0 49
      packages/ckeditor5-table/src/tableselection/converters.js
  11. 1 1
      packages/ckeditor5-table/src/tableselection/croptable.js
  12. 0 160
      packages/ckeditor5-table/src/tableselection/mouseselectionhandler.js
  13. 51 0
      packages/ckeditor5-table/src/tableselection/utils.js
  14. 12 2
      packages/ckeditor5-table/src/ui/utils.js
  15. 58 3
      packages/ckeditor5-table/tests/commands/insertcolumncommand.js
  16. 67 2
      packages/ckeditor5-table/tests/commands/insertrowcommand.js
  17. 18 7
      packages/ckeditor5-table/tests/manual/tableselection.html
  18. 4 1
      packages/ckeditor5-table/tests/manual/tableselection.js
  19. 2 2
      packages/ckeditor5-table/tests/manual/tableselection.md
  20. 4 2
      packages/ckeditor5-table/tests/table.js
  21. 107 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbackgroundcolorcommand.js
  22. 107 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbordercolorcommand.js
  23. 113 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderstylecommand.js
  24. 113 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderwidthcommand.js
  25. 107 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellheightcommand.js
  26. 107 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js
  27. 107 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellpaddingcommand.js
  28. 107 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellverticalalignmentcommand.js
  29. 107 1
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellwidthcommand.js
  30. 15 1
      packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js
  31. 155 101
      packages/ckeditor5-table/tests/tableclipboard.js
  32. 231 0
      packages/ckeditor5-table/tests/tableselection-integration.js
  33. 44 237
      packages/ckeditor5-table/tests/tableselection.js
  34. 0 485
      packages/ckeditor5-table/tests/tableselection/mouseselectionhandler.js
  35. 1 1
      packages/ckeditor5-table/theme/table.css
  36. 0 7
      packages/ckeditor5-table/theme/tableselection.css

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

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { findAncestor } from './utils';
+import { getRangeContainedElement, findAncestor } from './utils';
 
 /**
  * The insert column command.
@@ -70,15 +70,16 @@ export default class InsertColumnCommand extends Command {
 		const editor = this.editor;
 		const selection = editor.model.document.selection;
 		const tableUtils = editor.plugins.get( 'TableUtils' );
+		const insertBefore = this.order === 'left';
 
-		const firstPosition = selection.getFirstPosition();
+		const referencePosition = insertBefore ? selection.getFirstPosition() : selection.getLastPosition();
+		const referenceRange = insertBefore ? selection.getFirstRange() : selection.getLastRange();
 
-		const tableCell = findAncestor( 'tableCell', firstPosition );
+		const tableCell = getRangeContainedElement( referenceRange ) || findAncestor( 'tableCell', referencePosition );
 		const table = tableCell.parent.parent;
 
 		const { column } = tableUtils.getCellLocation( tableCell );
-		const insertAt = this.order === 'right' ? column + 1 : column;
 
-		tableUtils.insertColumns( table, { columns: 1, at: insertAt } );
+		tableUtils.insertColumns( table, { columns: 1, at: insertBefore ? column : column + 1 } );
 	}
 }

+ 7 - 4
packages/ckeditor5-table/src/commands/insertrowcommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { findAncestor } from './utils';
+import { getRangeContainedElement, findAncestor } from './utils';
 
 /**
  * The insert row command.
@@ -69,14 +69,17 @@ export default class InsertRowCommand extends Command {
 		const editor = this.editor;
 		const selection = editor.model.document.selection;
 		const tableUtils = editor.plugins.get( 'TableUtils' );
+		const insertAbove = this.order === 'above';
 
-		const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() );
+		const referencePosition = insertAbove ? selection.getFirstPosition() : selection.getLastPosition();
+		const referenceRange = insertAbove ? selection.getFirstRange() : selection.getLastRange();
+
+		const tableCell = getRangeContainedElement( referenceRange ) || findAncestor( 'tableCell', referencePosition );
 		const tableRow = tableCell.parent;
 		const table = tableRow.parent;
 
 		const row = table.getChildIndex( tableRow );
-		const insertAt = this.order === 'below' ? row + 1 : row;
 
-		tableUtils.insertRows( table, { rows: 1, at: insertAt } );
+		tableUtils.insertRows( table, { rows: 1, at: this.order === 'below' ? row + 1 : row } );
 	}
 }

+ 13 - 0
packages/ckeditor5-table/src/commands/utils.js

@@ -29,6 +29,19 @@ export function findAncestor( parentName, positionOrElement ) {
 	}
 }
 
+/**
+ * Returns an element contained by a range, if it's the only one element contained by the range and if it's fully contained.
+ *
+ * @param {module:engine/model/range~Range} range
+ * @returns {module:engine/model/element~Element|null}
+ */
+export function getRangeContainedElement( range ) {
+	const nodeAfterStart = range.start.nodeAfter;
+	const nodeBeforeEnd = range.end.nodeBefore;
+
+	return ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
+}
+
 /**
  * A common method to update the numeric value. If a value is the default one, it will be unset.
  *

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

@@ -11,6 +11,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import TableEditing from './tableediting';
 import TableUI from './tableui';
+import TableSelection from './tableselection';
+import TableClipboard from './tableclipboard';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 
 import '../theme/table.css';
@@ -20,8 +22,12 @@ import '../theme/table.css';
  *
  * For a detailed overview, check the {@glink features/table Table feature documentation}.
  *
- * This is a "glue" plugin that loads the {@link module:table/tableediting~TableEditing table editing feature}
- * and {@link module:table/tableui~TableUI table UI feature}.
+ * This is a "glue" plugin that loads the following table features:
+ *
+ * * {@link module:table/tableediting~TableEditing editing feature},
+ * * {@link module:table/tableselection~TableSelection selection feature},
+ * * {@link module:table/tableclipboar~TableClipboard clipboard feature},
+ * * {@link module:table/tableui~TableUI UI feature}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -30,7 +36,7 @@ export default class Table extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ TableEditing, TableUI, Widget ];
+		return [ TableEditing, TableUI, TableSelection, TableClipboard, Widget ];
 	}
 
 	/**

+ 31 - 7
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js

@@ -36,12 +36,11 @@ export default class TableCellPropertyCommand extends Command {
 	 */
 	refresh() {
 		const editor = this.editor;
-		const selection = editor.model.document.selection;
 
-		const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() );
+		const selectedTableCells = getSelectedTableCells( editor.model );
 
-		this.isEnabled = !!tableCell;
-		this.value = this._getAttribute( tableCell );
+		this.isEnabled = !!selectedTableCells.length;
+		this.value = this._getSingleValue( selectedTableCells );
 	}
 
 	/**
@@ -56,12 +55,10 @@ export default class TableCellPropertyCommand extends Command {
 	 */
 	execute( options = {} ) {
 		const model = this.editor.model;
-		const selection = model.document.selection;
 
 		const { value, batch } = options;
 
-		const tableCells = Array.from( selection.getSelectedBlocks() )
-			.map( element => findAncestor( 'tableCell', model.createPositionAt( element, 0 ) ) );
+		const tableCells = getSelectedTableCells( model );
 		const valueToSet = this._getValueToSet( value );
 
 		model.enqueueChange( batch || 'default', writer => {
@@ -98,4 +95,31 @@ export default class TableCellPropertyCommand extends Command {
 	_getValueToSet( value ) {
 		return value;
 	}
+
+	/**
+	 * Returns a single value for all selected table cells. If the value is the same for all cells,
+	 * it will be returned (`undefined` otherwise).
+	 *
+	 * @param {Array.<module:engine/model/element~Element>} tableCell
+	 * @returns {*}
+	 * @private
+	 */
+	_getSingleValue( tableCell ) {
+		const firstCellValue = this._getAttribute( tableCell[ 0 ] );
+
+		const everyCellHasAttribute = tableCell.every( tableCell => this._getAttribute( tableCell ) === firstCellValue );
+
+		return everyCellHasAttribute ? firstCellValue : undefined;
+	}
+}
+
+// Returns all selected table cells.
+// The implementation of this function is incorrect as it may return a single cell twice.
+// See https://github.com/ckeditor/ckeditor5/issues/6358.
+function getSelectedTableCells( model ) {
+	const selection = model.document.selection;
+
+	return Array.from( selection.getSelectedBlocks() )
+		.map( element => findAncestor( 'tableCell', model.createPositionAt( element, 0 ) ) )
+		.filter( tableCell => !!tableCell );
 }

+ 3 - 4
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js

@@ -69,9 +69,8 @@ export default class TableCellPropertiesEditing extends Plugin {
 		const editor = this.editor;
 		const schema = editor.model.schema;
 		const conversion = editor.conversion;
-		const viewDoc = editor.editing.view.document;
 
-		viewDoc.addStyleProcessorRules( addBorderRules );
+		editor.data.addStyleProcessorRules( addBorderRules );
 		enableBorderProperties( schema, conversion );
 		editor.commands.add( 'tableCellBorderStyle', new TableCellBorderStyleCommand( editor ) );
 		editor.commands.add( 'tableCellBorderColor', new TableCellBorderColorCommand( editor ) );
@@ -86,11 +85,11 @@ export default class TableCellPropertiesEditing extends Plugin {
 		enableProperty( schema, conversion, 'height', 'height' );
 		editor.commands.add( 'tableCellHeight', new TableCellHeightCommand( editor ) );
 
-		viewDoc.addStyleProcessorRules( addPaddingRules );
+		editor.data.addStyleProcessorRules( addPaddingRules );
 		enableProperty( schema, conversion, 'padding', 'padding' );
 		editor.commands.add( 'tableCellPadding', new TableCellPaddingCommand( editor ) );
 
-		viewDoc.addStyleProcessorRules( addBackgroundRules );
+		editor.data.addStyleProcessorRules( addBackgroundRules );
 		enableProperty( schema, conversion, 'backgroundColor', 'background-color' );
 		editor.commands.add( 'tableCellBackgroundColor', new TableCellBackgroundColorCommand( editor ) );
 

+ 6 - 29
packages/ckeditor5-table/src/tableclipboard.js

@@ -39,30 +39,21 @@ export default class TableClipboard extends Plugin {
 		const editor = this.editor;
 		const viewDocument = editor.editing.view.document;
 
-		/**
-		 * A table selection plugin instance.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:table/tableselection~TableSelection} module:tableclipboard~TableClipboard#_tableSelection
-		 */
-		this._tableSelection = editor.plugins.get( 'TableSelection' );
-
-		this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopy( evt, data ), { priority: 'normal' } );
-		this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCut( evt, data ), { priority: 'high' } );
+		this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) );
+		this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) );
 	}
 
 	/**
-	 * A clipboard "copy" event handler.
+	 * Copies table content to a clipboard on "copy" & "cut" events.
 	 *
 	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event.
 	 * @param {Object} data Clipboard event data.
 	 * @private
 	 */
-	_onCopy( evt, data ) {
-		const tableSelection = this._tableSelection;
+	_onCopyCut( evt, data ) {
+		const tableSelection = this.editor.plugins.get( 'TableSelection' );
 
-		if ( !tableSelection.hasMultiCellSelection ) {
+		if ( !tableSelection.getSelectedTableCells() ) {
 			return;
 		}
 
@@ -80,18 +71,4 @@ export default class TableClipboard extends Plugin {
 			method: evt.name
 		} );
 	}
-
-	/**
-	 * A clipboard "cut" event handler.
-	 *
-	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event.
-	 * @param {Object} data Clipboard event data.
-	 * @private
-	 */
-	_onCut( evt, data ) {
-		if ( this._tableSelection.hasMultiCellSelection ) {
-			data.preventDefault();
-			evt.stop();
-		}
-	}
 }

+ 2 - 3
packages/ckeditor5-table/src/tableproperties/tablepropertiesediting.js

@@ -69,9 +69,8 @@ export default class TablePropertiesEditing extends Plugin {
 		const editor = this.editor;
 		const schema = editor.model.schema;
 		const conversion = editor.conversion;
-		const viewDoc = editor.editing.view.document;
 
-		viewDoc.addStyleProcessorRules( addBorderRules );
+		editor.data.addStyleProcessorRules( addBorderRules );
 		enableBorderProperties( schema, conversion );
 		editor.commands.add( 'tableBorderColor', new TableBorderColorCommand( editor ) );
 		editor.commands.add( 'tableBorderStyle', new TableBorderStyleCommand( editor ) );
@@ -86,7 +85,7 @@ export default class TablePropertiesEditing extends Plugin {
 		enableTableToFigureProperty( schema, conversion, 'height', 'height' );
 		editor.commands.add( 'tableHeight', new TableHeightCommand( editor ) );
 
-		viewDoc.addStyleProcessorRules( addBackgroundRules );
+		editor.data.addStyleProcessorRules( addBackgroundRules );
 		enableProperty( schema, conversion, 'backgroundColor', 'background-color' );
 		editor.commands.add( 'tableBackgroundColor', new TableBackgroundColorCommand( editor ) );
 	}

+ 305 - 167
packages/ckeditor5-table/src/tableselection.js

@@ -11,34 +11,15 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import TableWalker from './tablewalker';
 import TableUtils from './tableutils';
-import { setupTableSelectionHighlighting } from './tableselection/converters';
-import MouseSelectionHandler from './tableselection/mouseselectionhandler';
+import MouseEventsObserver from './tableselection/mouseeventsobserver';
+import { getTableCellsInSelection, clearTableCellsContents } from './tableselection/utils';
 import { findAncestor } from './commands/utils';
 import cropTable from './tableselection/croptable';
 
 import '../theme/tableselection.css';
 
 /**
- * The table selection plugin.
- *
- * It introduces the ability to select table cells. The table selection is described by two nodes: start and end.
- * Both are the oposite corners of an rectangle that spans over them.
- *
- * Consider a table:
- *
- *		    0   1   2   3
- *		  +---+---+---+---+
- *		0 | a | b | c | d |
- *		  +-------+   +---+
- *		1 | e | f |   | g |
- *		  +---+---+---+---+
- *		2 | h | i     | j |
- *		  +---+---+---+---+
- *
- * Setting the table selection start in table cell "b" and the end in table cell "g" will select table cells: "b", "c", "d", "f", and "g".
- * The cells that span over multiple rows or columns can extend over the selection rectangle. For instance, setting a selection from
- * the table cell "a" to the table cell "i" will create a selection in which the table cell "i" will be (partially) outside
- * the rectangle of selected cells: "a", "b", "e", "f", "h", and "i".
+ * TODO
  *
  * @extends module:core/plugin~Plugin
  */
@@ -57,226 +38,383 @@ export default class TableSelection extends Plugin {
 		return [ TableUtils ];
 	}
 
-	/**
-	 * @inheritDoc
-	 */
-	constructor( editor ) {
-		super( editor );
-
-		/**
-		 * A mouse selection handler.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:table/tableselection/mouseselectionhandler~MouseSelectionHandler}
-		 */
-		this._mouseHandler = new MouseSelectionHandler( this, this.editor.editing );
-
-		/**
-		 * A reference to the table utilities used across the class.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:table/tableutils~TableUtils} #_tableUtils
-		 */
-	}
-
-	/**
-	 * A flag indicating that there are selected table cells and the selection includes more than one table cell.
-	 *
-	 * @type {Boolean}
-	 */
-	get hasMultiCellSelection() {
-		return !!this._startElement && !!this._endElement && this._startElement !== this._endElement;
-	}
-
 	/**
 	 * @inheritDoc
 	 */
 	init() {
 		const editor = this.editor;
-		const selection = editor.model.document.selection;
-
-		this._tableUtils = editor.plugins.get( 'TableUtils' );
+		const model = editor.model;
 
-		setupTableSelectionHighlighting( editor, this );
+		this.listenTo( model, 'deleteContent', ( evt, args ) => this._handleDeleteContent( evt, args ), { priority: 'high' } );
 
-		selection.on( 'change:range', () => this._clearSelectionOnExternalChange( selection ) );
-	}
+		// Currently the MouseObserver only handles `mouseup` events.
+		// TODO move to the engine?
+		editor.editing.view.addObserver( MouseEventsObserver );
 
-	/**
-	 * @inheritDoc
-	 */
-	destroy() {
-		super.destroy();
-		this._mouseHandler.stopListening();
+		this._defineSelectionConverter();
+		this._enableShiftClickSelection();
+		this._enableMouseDragSelection();
 	}
 
 	/**
-	 * Marks the table cell as a start of a table selection.
-	 *
-	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( tableCell );
+	 * Returns currently selected table cells or `null` if not a table cells selection.
 	 *
-	 * This method will clear the previous selection. The model selection will not be updated until
-	 * the {@link #setSelectingTo} method is used.
-	 *
-	 * @param {module:engine/model/element~Element} tableCell
+	 * @returns {Array.<module:engine/model/element~Element>|null}
 	 */
-	startSelectingFrom( tableCell ) {
-		this.clearSelection();
+	getSelectedTableCells() {
+		const selection = this.editor.model.document.selection;
+
+		const selectedCells = getTableCellsInSelection( selection );
 
-		this._startElement = tableCell;
-		this._endElement = tableCell;
+		if ( selectedCells.length == 0 ) {
+			return null;
+		}
+
+		// This should never happen, but let's know if it ever happens.
+		// @if CK_DEBUG //	if ( selectedCells.length != selection.rangeCount ) {
+		// @if CK_DEBUG //		console.warn( 'Mixed selection warning. The selection contains table cells and some other ranges.' );
+		// @if CK_DEBUG //	}
+
+		return selectedCells;
 	}
 
 	/**
-	 * Updates the current table selection end element. Table selection is defined by a start and an end element.
-	 * This method updates the end element. Must be preceded by {@link #startSelectingFrom}.
-	 *
-	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
+	 * Returns a selected table fragment as a document fragment.
 	 *
-	 *		editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
-	 *
-	 * This method will update model selection if start and end cells are different and belongs to the same table.
-	 *
-	 * @param {module:engine/model/element~Element} tableCell
+	 * @returns {module:engine/model/documentfragment~DocumentFragment|null}
 	 */
-	setSelectingTo( tableCell ) {
-		if ( !this._startElement ) {
-			this._startElement = tableCell;
+	getSelectionAsFragment() {
+		const selectedCells = this.getSelectedTableCells();
+
+		if ( !selectedCells ) {
+			return null;
 		}
 
-		const table = this._startElement.parent.parent;
+		return this.editor.model.change( writer => {
+			const documentFragment = writer.createDocumentFragment();
+			const table = cropTable( selectedCells, this.editor.plugins.get( 'TableUtils' ), writer );
 
-		// Do not add tableCell to selection if it is from other table or is already set as end element.
-		if ( table !== tableCell.parent.parent || this._endElement === tableCell ) {
-			return;
-		}
+			writer.insert( table, documentFragment, 0 );
 
-		this._endElement = tableCell;
-		this._updateModelSelection();
+			return documentFragment;
+		} );
 	}
 
 	/**
-	 * Stops the selection process (but do not clear the current selection).
-	 * The selection process is finished but the selection in the model remains.
+	 * Defines a selection converter which marks selected cells with a specific class.
 	 *
-	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
-	 *		editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
-	 *		editor.plugins.get( 'TableSelection' ).stopSelection();
+	 * The real DOM selection is put in the last cell. Since the order of ranges is dependent on whether the
+	 * selection is backward or not, the last cell with usually be close to the "focus" end of the selection
+	 * (a selection has anchor and focus).
 	 *
-	 * To clear the selection use {@link #clearSelection}.
+	 * The real DOM selection is then hidden with CSS.
 	 *
-	 * @param {module:engine/model/element~Element} [tableCell]
+	 * @private
 	 */
-	stopSelection( tableCell ) {
-		if ( tableCell && tableCell.parent.parent === this._startElement.parent.parent ) {
-			this._endElement = tableCell;
-		}
+	_defineSelectionConverter() {
+		const editor = this.editor;
+		const highlighted = new Set();
+
+		editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+
+			clearHighlightedTableCells( viewWriter );
+
+			const selectedCells = this.getSelectedTableCells();
+
+			if ( !selectedCells ) {
+				return;
+			}
+
+			for ( const tableCell of selectedCells ) {
+				const viewElement = conversionApi.mapper.toViewElement( tableCell );
+
+				viewWriter.addClass( 'ck-editor__editable_selected', viewElement );
+				highlighted.add( viewElement );
+			}
+
+			const lastViewCell = conversionApi.mapper.toViewElement( selectedCells[ selectedCells.length - 1 ] );
+			viewWriter.setSelection( lastViewCell, 0 );
+		}, { priority: 'lowest' } ) );
 
-		this._updateModelSelection();
+		function clearHighlightedTableCells( writer ) {
+			for ( const previouslyHighlighted of highlighted ) {
+				writer.removeClass( 'ck-editor__editable_selected', previouslyHighlighted );
+			}
+
+			highlighted.clear();
+		}
 	}
 
 	/**
-	 * Stops the current selection process and clears the table selection in the model.
-	 *
-	 *		editor.plugins.get( 'TableSelection' ).startSelectingFrom( startTableCell );
-	 *		editor.plugins.get( 'TableSelection' ).setSelectingTo( endTableCell );
-	 *		editor.plugins.get( 'TableSelection' ).stopSelection();
+	 * Enables making cells selection by Shift+click. Creates a selection from the cell which previously hold
+	 * the selection to the cell which was clicked (can be the same cell, in which case it selects a single cell).
 	 *
-	 *		editor.plugins.get( 'TableSelection' ).clearSelection();
+	 * @private
 	 */
-	clearSelection() {
-		this._startElement = undefined;
-		this._endElement = undefined;
+	_enableShiftClickSelection() {
+		const editor = this.editor;
+		let blockSelectionChange = false;
+
+		this.listenTo( editor.editing.view.document, 'mousedown', ( evt, domEventData ) => {
+			if ( !domEventData.domEvent.shiftKey ) {
+				return;
+			}
+
+			const anchorCell = findAncestor( 'tableCell', editor.model.document.selection.anchor.parent );
+
+			if ( !anchorCell ) {
+				return;
+			}
+
+			const targetCell = this._getModelTableCellFromDomEvent( domEventData );
+
+			if ( targetCell && haveSameTableParent( anchorCell, targetCell ) ) {
+				blockSelectionChange = true;
+				this._setCellSelection( anchorCell, targetCell );
+
+				domEventData.preventDefault();
+			}
+		} );
+
+		this.listenTo( editor.editing.view.document, 'mouseup', () => {
+			blockSelectionChange = false;
+		} );
+
+		// We need to ignore a `selectionChange` event that is fired after we render our new table cells selection.
+		// When downcasting table cells selection to the view, we put the view selection in the last selected cell
+		// in a place that may not be natively a "correct" location. This is – we put it directly in the `<td>` element.
+		// All browsers fire the native `selectionchange` event.
+		// However, all browsers except Safari return the selection in the exact place where we put it
+		// (even though it's visually normalized). Safari returns `<td><p>^foo` that makes our selection observer
+		// fire our `selectionChange` event (because the view selection that we set in the first step differs from the DOM selection).
+		// Since `selectionChange` is fired, we automatically update the model selection that moves it that paragraph.
+		// This breaks our dear cells selection.
+		//
+		// Theoretically this issue concerns only Safari that is the only browser that do normalize the selection.
+		// However, to avoid code branching and to have a good coverage for this event blocker, I enabled it for all browsers.
+		//
+		// Note: I'm keeping the `blockSelectionChange` state separately for shift+click and mouse drag (exact same logic)
+		// so I don't have to try to analyze whether they don't overlap in some weird cases. Probably they don't.
+		// But I have other things to do, like writing this comment.
+		this.listenTo( editor.editing.view.document, 'selectionChange', evt => {
+			if ( blockSelectionChange ) {
+				// @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );
+
+				evt.stop();
+			}
+		}, { priority: 'highest' } );
 	}
 
 	/**
-	 * Returns an iterator for selected table cells.
-	 *
-	 *		tableSelection.startSelectingFrom( startTableCell );
-	 *		tableSelection.stopSelection( endTableCell );
+	 * Enables making cells selection by dragging.
 	 *
-	 *		const selectedTableCells = Array.from( tableSelection.getSelectedTableCells() );
-	 *		// The above array will represent a rectangular table selection.
+	 * The selection is made only on mousemove. We start tracking the mouse on mousedown.
+	 * However, the cells selection is enabled only after the mouse cursor left the anchor cell.
+	 * Thanks to that normal text selection within one cell works just fine. However, you can still select
+	 * just one cell by leaving the anchor cell and moving back to it.
 	 *
-	 * @returns {Iterable.<module:engine/model/element~Element>}
+	 * @private
 	 */
-	* getSelectedTableCells() {
-		if ( !this.hasMultiCellSelection ) {
-			return;
-		}
+	_enableMouseDragSelection() {
+		const editor = this.editor;
+		let anchorCell, targetCell;
+		let beganCellSelection = false;
+		let blockSelectionChange = false;
+
+		this.listenTo( editor.editing.view.document, 'mousedown', ( evt, domEventData ) => {
+			// Make sure to not conflict with the shift+click listener and any other possible handler.
+			if ( domEventData.domEvent.shiftKey || domEventData.domEvent.ctrlKey || domEventData.domEvent.altKey ) {
+				return;
+			}
 
-		const startLocation = this._tableUtils.getCellLocation( this._startElement );
-		const endLocation = this._tableUtils.getCellLocation( this._endElement );
+			anchorCell = this._getModelTableCellFromDomEvent( domEventData );
+		} );
 
-		const startRow = Math.min( startLocation.row, endLocation.row );
-		const endRow = Math.max( startLocation.row, endLocation.row );
+		this.listenTo( editor.editing.view.document, 'mousemove', ( evt, domEventData ) => {
+			if ( !domEventData.domEvent.buttons ) {
+				return;
+			}
 
-		const startColumn = Math.min( startLocation.column, endLocation.column );
-		const endColumn = Math.max( startLocation.column, endLocation.column );
+			if ( !anchorCell ) {
+				return;
+			}
 
-		for ( const cellInfo of new TableWalker( findAncestor( 'table', this._startElement ), { startRow, endRow } ) ) {
-			if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
-				yield cellInfo.cell;
+			const newTargetCell = this._getModelTableCellFromDomEvent( domEventData );
+
+			if ( newTargetCell && haveSameTableParent( anchorCell, newTargetCell ) ) {
+				targetCell = newTargetCell;
+
+				// Switch to the cell selection mode after the mouse cursor left the anchor cell.
+				// Switch off only on mouseup (makes selecting a single cell possible).
+				if ( !beganCellSelection && targetCell != anchorCell ) {
+					beganCellSelection = true;
+				}
 			}
-		}
+
+			// Yep, not making a cell selection yet. See method docs.
+			if ( !beganCellSelection ) {
+				return;
+			}
+
+			blockSelectionChange = true;
+			this._setCellSelection( anchorCell, targetCell );
+
+			domEventData.preventDefault();
+		} );
+
+		this.listenTo( editor.editing.view.document, 'mouseup', () => {
+			beganCellSelection = false;
+			blockSelectionChange = false;
+			anchorCell = null;
+			targetCell = null;
+		} );
+
+		// See the explanation in `_enableShiftClickSelection()`.
+		this.listenTo( editor.editing.view.document, 'selectionChange', evt => {
+			if ( blockSelectionChange ) {
+				// @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );
+
+				evt.stop();
+			}
+		}, { priority: 'highest' } );
 	}
 
 	/**
-	 * Returns selected table fragment as a document fragment.
+	 * It overrides the default `model.deleteContent()` behavior over a selected table fragment.
 	 *
-	 * @returns {module:engine/model/documentfragment~DocumentFragment|undefined}
+	 * @private
+	 * @param {module:utils/eventinfo~EventInfo} event
+	 * @param {Array.<*>} args Delete content method arguments.
 	 */
-	getSelectionAsFragment() {
-		if ( !this.hasMultiCellSelection ) {
+	_handleDeleteContent( event, args ) {
+		const [ selection, options ] = args;
+		const model = this.editor.model;
+		const isBackward = !options || options.direction == 'backward';
+		const selectedTableCells = getTableCellsInSelection( selection );
+
+		if ( !selectedTableCells.length ) {
 			return;
 		}
 
-		return this.editor.model.change( writer => {
-			const documentFragment = writer.createDocumentFragment();
+		event.stop();
 
-			const table = cropTable( this.getSelectedTableCells(), this._tableUtils, writer );
-			writer.insert( table, documentFragment, 0 );
+		model.change( writer => {
+			const tableCellToSelect = selectedTableCells[ isBackward ? selectedTableCells.length - 1 : 0 ];
 
-			return documentFragment;
+			clearTableCellsContents( model, selectedTableCells );
+
+			// The insertContent() helper passes the actual DocumentSelection,
+			// while the deleteContent() helper always operates on the abstract clones.
+			if ( selection.is( 'documentSelection' ) ) {
+				writer.setSelection( tableCellToSelect, 'in' );
+			} else {
+				selection.setTo( tableCellToSelect, 'in' );
+			}
 		} );
 	}
 
 	/**
-	 * Synchronizes the model selection with currently selected table cells.
+	 * Sets the model selection based on given anchor and target cells (can be the same cell).
+	 * Takes care of setting backward flag.
+	 *
+	 * @protected
+	 * @param {module:engine/model/element~Element} anchorCell
+	 * @param {module:engine/model/element~Element} targetCell
+	 */
+	_setCellSelection( anchorCell, targetCell ) {
+		const cellsToSelect = this._getCellsToSelect( anchorCell, targetCell );
+
+		this.editor.model.change( writer => {
+			writer.setSelection(
+				cellsToSelect.cells.map( cell => writer.createRangeOn( cell ) ),
+				{ backward: cellsToSelect.backward }
+			);
+		} );
+	}
+
+	/**
+	 * Returns the model table cell element based on the target element of the passed DOM event.
 	 *
 	 * @private
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 * @returns {module:engine/model/element~Element|undefined} Returns the table cell or `undefined`.
 	 */
-	_updateModelSelection() {
-		if ( !this.hasMultiCellSelection ) {
+	_getModelTableCellFromDomEvent( domEventData ) {
+		// Note: Work with positions (not element mapping) because the target element can be an attribute or other non-mapped element.
+		const viewTargetElement = domEventData.target;
+		const viewPosition = this.editor.editing.view.createPositionAt( viewTargetElement, 0 );
+		const modelPosition = this.editor.editing.mapper.toModelPosition( viewPosition );
+		const modelElement = modelPosition.parent;
+
+		if ( !modelElement ) {
 			return;
 		}
 
-		const editor = this.editor;
-		const model = editor.model;
-
-		const modelRanges = [];
-
-		for ( const tableCell of this.getSelectedTableCells() ) {
-			modelRanges.push( model.createRangeOn( tableCell ) );
+		if ( modelElement.is( 'tableCell' ) ) {
+			return modelElement;
 		}
 
-		// Update model's selection
-		model.change( writer => {
-			writer.setSelection( modelRanges );
-		} );
+		return findAncestor( 'tableCell', modelElement );
 	}
 
 	/**
-	 * Checks if the selection has changed via an external change and if it is required to clear the internal state of the plugin.
+	 * Returns an array of table cells that should be selected based on the
+	 * given anchor cell and target (focus) cell.
+	 *
+	 * The cells are returned in a reverse direction if the selection is backward.
 	 *
-	 * @param {module:engine/model/documentselection~DocumentSelection} selection
 	 * @private
+	 * @param {module:engine/model/element~Element} anchorCell
+	 * @param {module:engine/model/element~Element} targetCell
+	 * @returns {Array.<module:engine/model/element~Element>}
 	 */
-	_clearSelectionOnExternalChange( selection ) {
-		if ( selection.rangeCount <= 1 && this.hasMultiCellSelection ) {
-			this.clearSelection();
+	_getCellsToSelect( anchorCell, targetCell ) {
+		const tableUtils = this.editor.plugins.get( 'TableUtils' );
+		const startLocation = tableUtils.getCellLocation( anchorCell );
+		const endLocation = tableUtils.getCellLocation( targetCell );
+
+		const startRow = Math.min( startLocation.row, endLocation.row );
+		const endRow = Math.max( startLocation.row, endLocation.row );
+
+		const startColumn = Math.min( startLocation.column, endLocation.column );
+		const endColumn = Math.max( startLocation.column, endLocation.column );
+
+		const cells = [];
+
+		for ( const cellInfo of new TableWalker( findAncestor( 'table', anchorCell ), { startRow, endRow } ) ) {
+			if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
+				cells.push( cellInfo.cell );
+			}
+		}
+
+		// A selection started in the bottom right corner and finished in the upper left corner
+		// should have it ranges returned in the reverse order.
+		// However, this is only half of the story because the selection could be made to the left (then the left cell is a focus)
+		// or to the right (then the right cell is a focus), while being a forward selection in both cases
+		// (because it was made from top to bottom). This isn't handled.
+		// This method would need to be smarter, but the ROI is microscopic, so I skip this.
+		if ( checkIsBackward( startLocation, endLocation ) ) {
+			return { cells: cells.reverse(), backward: true };
 		}
+
+		return { cells, backward: false };
 	}
 }
+
+// Naively check whether the selection should be backward or not. See the longer explanation where this function is used.
+function checkIsBackward( startLocation, endLocation ) {
+	if ( startLocation.row > endLocation.row ) {
+		return true;
+	}
+
+	if ( startLocation.row == endLocation.row && startLocation.column > endLocation.column ) {
+		return true;
+	}
+
+	return false;
+}
+
+function haveSameTableParent( cellA, cellB ) {
+	return cellA.parent.parent == cellB.parent.parent;
+}

+ 0 - 49
packages/ckeditor5-table/src/tableselection/converters.js

@@ -1,49 +0,0 @@
-/**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module table/tableselection/converters
- */
-
-/**
- * Adds a visual highlight style to selected table cells.
- *
- * @param {module:core/editor/editor~Editor} editor
- * @param {module:table/tableselection~TableSelection} tableSelection
- */
-export function setupTableSelectionHighlighting( editor, tableSelection ) {
-	const highlighted = new Set();
-
-	editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
-		const view = editor.editing.view;
-		const viewWriter = conversionApi.writer;
-		const viewSelection = viewWriter.document.selection;
-
-		if ( tableSelection.hasMultiCellSelection ) {
-			clearHighlightedTableCells( highlighted, view );
-
-			for ( const tableCell of tableSelection.getSelectedTableCells() ) {
-				const viewElement = conversionApi.mapper.toViewElement( tableCell );
-
-				viewWriter.addClass( 'ck-editor__editable_selected', viewElement );
-				highlighted.add( viewElement );
-			}
-
-			viewWriter.setSelection( viewSelection.getRanges(), { fake: true, label: 'TABLE' } );
-		} else {
-			clearHighlightedTableCells( highlighted, view );
-		}
-	}, { priority: 'lowest' } ) );
-}
-
-function clearHighlightedTableCells( highlighted, view ) {
-	const previous = [ ...highlighted.values() ];
-
-	view.change( writer => {
-		for ( const previouslyHighlighted of previous ) {
-			writer.removeClass( 'ck-editor__editable_selected', previouslyHighlighted );
-		}
-	} );
-}

+ 1 - 1
packages/ckeditor5-table/src/tableselection/croptable.js

@@ -17,7 +17,7 @@ import { findAncestor } from '../commands/utils';
  *		tableSelection.startSelectingFrom( startCell )
  *		tableSelection.setSelectingFrom( endCell )
  *
- *		const croppedTable = cropTable( tableSelection.getSelectedTableCells );
+ *		const croppedTable = cropTable( tableSelection.getSelectedTableCells() );
  *
  * **Note**: This function is used also by {@link module:table/tableselection~TableSelection#getSelectionAsFragment}
  *

+ 0 - 160
packages/ckeditor5-table/src/tableselection/mouseselectionhandler.js

@@ -1,160 +0,0 @@
-/**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module table/tableselection/mouseselectionhandler
- */
-
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-
-import { findAncestor } from '../commands/utils';
-import MouseEventsObserver from './mouseeventsobserver';
-
-/**
- * A mouse selection handler class for the table selection.
- *
- * It registers the {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} to observe view document mouse events
- * and invoke proper {@link module:table/tableselection~TableSelection} actions.
- */
-export default class MouseSelectionHandler {
-	/**
-	 * Creates an instance of the `MouseSelectionHandler`.
-	 *
-	 * @param {module:table/tableselection~TableSelection} tableSelection
-	 * @param {module:engine/controller/editingcontroller~EditingController} editing
-	 */
-	constructor( tableSelection, editing ) {
-		/**
-		 * The table selection plugin instance.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:table/tableselection~TableSelection}
-		 */
-		this._tableSelection = tableSelection;
-
-		/**
-		 * A flag indicating that the mouse selection is "active". A selection is "active" if it was started and not yet finished.
-		 * A selection can be "active", for instance, if a user moves a mouse over a table while holding a mouse button down.
-		 *
-		 * @readonly
-		 * @member {Boolean}
-		 */
-		this.isSelecting = false;
-
-		/**
-		 * Editing mapper.
-		 *
-		 * @private
-		 * @readonly
-		 * @member {module:engine/conversion/mapper~Mapper}
-		 */
-		this._mapper = editing.mapper;
-
-		const view = editing.view;
-
-		// Currently the MouseObserver only handles `mouseup` events.
-		view.addObserver( MouseEventsObserver );
-
-		this.listenTo( view.document, 'mousedown', ( event, domEventData ) => this._handleMouseDown( domEventData ) );
-		this.listenTo( view.document, 'mousemove', ( event, domEventData ) => this._handleMouseMove( domEventData ) );
-		this.listenTo( view.document, 'mouseup', ( event, domEventData ) => this._handleMouseUp( domEventData ) );
-	}
-
-	/**
-	 * Handles starting a selection when "mousedown" event has table cell as a DOM target.
-	 *
-	 * If there is no table cell in the event target, it will clear the previous selection.
-	 *
-	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
-	 * @private
-	 */
-	_handleMouseDown( domEventData ) {
-		const tableCell = this._getModelTableCellFromDomEvent( domEventData );
-
-		if ( !tableCell ) {
-			this._tableSelection.clearSelection();
-			this._tableSelection.stopSelection();
-
-			return;
-		}
-
-		this.isSelecting = true;
-		this._tableSelection.startSelectingFrom( tableCell );
-	}
-
-	/**
-	 * Handles updating the table selection when the "mousemove" event has a table cell as a DOM target.
-	 *
-	 * Does nothing if there is no table cell in event target or the selection has not been started yet.
-	 *
-	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
-	 * @private
-	 */
-	_handleMouseMove( domEventData ) {
-		if ( !isButtonPressed( domEventData ) ) {
-			this._tableSelection.stopSelection();
-
-			return;
-		}
-
-		const tableCell = this._getModelTableCellFromDomEvent( domEventData );
-
-		if ( !tableCell ) {
-			return;
-		}
-
-		this._tableSelection.setSelectingTo( tableCell );
-	}
-
-	/**
-	 * Handles ending (not clearing) the table selection on the "mouseup" event.
-	 *
-	 * Does nothing if the selection has not been started yet.
-	 *
-	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
-	 * @private
-	 */
-	_handleMouseUp( domEventData ) {
-		if ( !this.isSelecting ) {
-			return;
-		}
-
-		const tableCell = this._getModelTableCellFromDomEvent( domEventData );
-
-		// Selection can be stopped if table cell is undefined.
-		this._tableSelection.stopSelection( tableCell );
-	}
-
-	/**
-	 * Finds a model table cell for a given DOM event.
-	 *
-	 * @private
-	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
-	 * @returns {module:engine/model/element~Element|undefined} Returns model table cell or undefined event target is not
-	 * a mapped table cell.
-	 */
-	_getModelTableCellFromDomEvent( domEventData ) {
-		const viewTargetElement = domEventData.target;
-		const modelElement = this._mapper.toModelElement( viewTargetElement );
-
-		if ( !modelElement ) {
-			return;
-		}
-
-		if ( modelElement.is( 'tableCell' ) ) {
-			return modelElement;
-		}
-
-		return findAncestor( 'tableCell', modelElement );
-	}
-}
-
-mix( MouseSelectionHandler, ObservableMixin );
-
-function isButtonPressed( domEventData ) {
-	return !!domEventData.domEvent.buttons;
-}

+ 51 - 0
packages/ckeditor5-table/src/tableselection/utils.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/tableselection/utils
+ */
+
+import { getRangeContainedElement } from '../commands/utils';
+
+/**
+ * Clears contents of the passed table cells.
+ *
+ * This is to be used with table selection
+ *
+ *		tableSelection.startSelectingFrom( startCell )
+ *		tableSelection.setSelectingFrom( endCell )
+ *
+ *		clearTableCellsContents( editor.model, tableSelection.getSelectedTableCells() );
+ *
+ * @param {module:engine/model/model~Model} model
+ * @param {Iterable.<module:engine/model/element~Element>} tableCells
+ */
+export function clearTableCellsContents( model, tableCells ) {
+	model.change( writer => {
+		for ( const tableCell of tableCells ) {
+			model.deleteContent( writer.createSelection( tableCell, 'in' ) );
+		}
+	} );
+}
+
+/**
+ * Returns all model cells within the provided model selection.
+ *
+ * @param {Iterable.<module:engine/model/selection~Selection>} selection
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+export function getTableCellsInSelection( selection ) {
+	const cells = [];
+
+	for ( const range of selection.getRanges() ) {
+		const element = getRangeContainedElement( range );
+
+		if ( element && element.is( 'tableCell' ) ) {
+			cells.push( element );
+		}
+	}
+
+	return cells;
+}

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

@@ -81,8 +81,8 @@ export function getBalloonTablePositionData( editor ) {
  * @returns {module:utils/dom/position~Options}
  */
 export function getBalloonCellPositionData( editor ) {
-	const firstPosition = editor.model.document.selection.getFirstPosition();
-	const modelTableCell = findAncestor( 'tableCell', firstPosition );
+	// This is a bit naive. See https://github.com/ckeditor/ckeditor5/issues/6357.
+	const modelTableCell = getTableCellAtPosition( editor.model.document.selection.getFirstPosition() );
 	const viewTableCell = editor.editing.mapper.toViewElement( modelTableCell );
 
 	return {
@@ -468,3 +468,13 @@ function colorConfigToColorGridDefinitions( colorConfig ) {
 		}
 	} ) );
 }
+
+// Returns the first selected table cell from a multi-cell or in-cell selection.
+//
+// @param {module:engine/model/position~Position} position Document position.
+// @returns {module:engine/model/element~Element}
+function getTableCellAtPosition( position ) {
+	const isTableCellSelected = position.nodeAfter && position.nodeAfter.is( 'tableCell' );
+
+	return isTableCellSelected ? position.nodeAfter : findAncestor( 'tableCell', position );
+}

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

@@ -7,7 +7,8 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltestedit
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import InsertColumnCommand from '../../src/commands/insertcolumncommand';
-import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+import TableSelection from '../../src/tableselection';
+import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
@@ -17,7 +18,7 @@ describe( 'InsertColumnCommand', () => {
 	beforeEach( () => {
 		return ModelTestEditor
 			.create( {
-				plugins: [ TableUtils ]
+				plugins: [ TableUtils, TableSelection ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -76,7 +77,7 @@ describe( 'InsertColumnCommand', () => {
 				] ) );
 			} );
 
-			it( 'should insert columns at table end', () => {
+			it( 'should insert column at table end', () => {
 				setData( model, modelTable( [
 					[ '11', '12' ],
 					[ '21', '22[]' ]
@@ -90,6 +91,33 @@ describe( 'InsertColumnCommand', () => {
 				] ) );
 			} );
 
+			it( 'should insert column after a multi column selection', () => {
+				setData( model, modelTable( [
+					[ '11', '12', '13' ],
+					[ '21', '22', '23' ]
+				] ) );
+
+				const tableSelection = editor.plugins.get( TableSelection );
+				const modelRoot = model.document.getRoot();
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				command.execute();
+
+				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+					[ '11', '12', '', '13' ],
+					[ '21', '22', '', '23' ]
+				] ) );
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0, 0 ],
+					[ 1, 1, 0, 0 ]
+				] );
+			} );
+
 			it( 'should update table heading columns attribute when inserting column in headings section', () => {
 				setData( model, modelTable( [
 					[ '11[]', '12' ],
@@ -214,6 +242,33 @@ describe( 'InsertColumnCommand', () => {
 				] ) );
 			} );
 
+			it( 'should insert column before a multi column selection', () => {
+				setData( model, modelTable( [
+					[ '11', '12', '13' ],
+					[ '21', '22', '23' ]
+				] ) );
+
+				const tableSelection = editor.plugins.get( TableSelection );
+				const modelRoot = model.document.getRoot();
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				command.execute();
+
+				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+					[ '', '11', '12', '13' ],
+					[ '', '21', '22', '23' ]
+				] ) );
+
+				assertSelectedCells( model, [
+					[ 0, 1, 1, 0 ],
+					[ 0, 1, 1, 0 ]
+				] );
+			} );
+
 			it( 'should update table heading columns attribute when inserting column in headings section', () => {
 				setData( model, modelTable( [
 					[ '11', '12[]' ],

+ 67 - 2
packages/ckeditor5-table/tests/commands/insertrowcommand.js

@@ -7,7 +7,8 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltestedit
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import InsertRowCommand from '../../src/commands/insertrowcommand';
-import { defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+import TableSelection from '../../src/tableselection';
+import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
 import TableUtils from '../../src/tableutils';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
@@ -17,7 +18,7 @@ describe( 'InsertRowCommand', () => {
 	beforeEach( () => {
 		return ModelTestEditor
 			.create( {
-				plugins: [ TableUtils ]
+				plugins: [ TableUtils, TableSelection ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -181,6 +182,38 @@ describe( 'InsertRowCommand', () => {
 					[ '', '' ]
 				] ) );
 			} );
+
+			it( 'should insert a row when multiple rows are selected', () => {
+				setData( model, modelTable( [
+					[ '11', '12' ],
+					[ '21', '22' ],
+					[ '31', '32' ]
+				] ) );
+
+				const tableSelection = editor.plugins.get( TableSelection );
+				const modelRoot = model.document.getRoot();
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				command.execute();
+
+				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+					[ '11', '12' ],
+					[ '21', '22' ],
+					[ '', '' ],
+					[ '31', '32' ]
+				] ) );
+
+				assertSelectedCells( model, [
+					[ 1, 1 ],
+					[ 1, 1 ],
+					[ 0, 0 ],
+					[ 0, 0 ]
+				] );
+			} );
 		} );
 	} );
 
@@ -284,6 +317,38 @@ describe( 'InsertRowCommand', () => {
 					[ '20[]', '21' ]
 				], { headingRows: 2 } ) );
 			} );
+
+			it( 'should insert a row when multiple rows are selected', () => {
+				setData( model, modelTable( [
+					[ '11', '12' ],
+					[ '21', '22' ],
+					[ '31', '32' ]
+				] ) );
+
+				const tableSelection = editor.plugins.get( TableSelection );
+				const modelRoot = model.document.getRoot();
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				command.execute();
+
+				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
+					[ '', '' ],
+					[ '11', '12' ],
+					[ '21', '22' ],
+					[ '31', '32' ]
+				] ) );
+
+				assertSelectedCells( model, [
+					[ 0, 0 ],
+					[ 1, 1 ],
+					[ 1, 1 ],
+					[ 0, 0 ]
+				] );
+			} );
 		} );
 	} );
 } );

+ 18 - 7
packages/ckeditor5-table/tests/manual/tableselection.html

@@ -12,6 +12,11 @@
 	.external-source td, .external-source th {
 		border: solid 1px hsl(0, 0%, 85%);
 	}
+
+	/* It should not create entire viewport scroll. */
+	pre {
+		overflow: auto;
+	}
 </style>
 
 <h3>A "content" test editor</h3>
@@ -52,7 +57,13 @@
 					<td>k</td>
 					<td>l</td>
 					<td><strong>m</strong></td>
-					<td>n</td>
+					<td>
+						<p>n</p>
+						<figure class="image image-style-side">
+							<img src="sample.jpg" />
+							<figcaption>Caption!</figcaption>
+						</figure>
+					</td>
 					<td>o</td>
 				</tr>
 				<tr>
@@ -110,7 +121,7 @@
 					<td rowspan="4">02</td>
 					<td>03</td>
 					<td colspan="2" rowspan="7">04</td>
-					<td>07</td>
+					<td>06</td>
 					<td>07</td>
 					<td>08</td>
 				</tr>
@@ -118,7 +129,7 @@
 					<td>10</td>
 					<td>11</td>
 					<td>13</td>
-					<td>17</td>
+					<td>16</td>
 					<td>17</td>
 					<td>18</td>
 				</tr>
@@ -126,18 +137,18 @@
 					<td>20</td>
 					<td>21</td>
 					<td>23</td>
-					<td colspan="3">27</td>
+					<td colspan="3">26</td>
 				</tr>
 				<tr>
 					<td>30</td>
 					<td>31</td>
 					<td>33</td>
-					<td>37</td>
+					<td>36</td>
 					<td colspan="2">37</td>
 				</tr>
 				<tr>
 					<td colspan="4">40</td>
-					<td>47</td>
+					<td>46</td>
 					<td>47</td>
 					<td>48</td>
 				</tr>
@@ -146,7 +157,7 @@
 					<td>51</td>
 					<td>52</td>
 					<td>53</td>
-					<td rowspan="4">57</td>
+					<td rowspan="4">56</td>
 					<td>57</td>
 					<td>58</td>
 				</tr>

+ 4 - 1
packages/ckeditor5-table/tests/manual/tableselection.js

@@ -31,13 +31,16 @@ function createEditor( target, inspectorName ) {
 				'bulletedList', 'numberedList', 'blockQuote', '|',
 				'undo', 'redo'
 			],
+			image: {
+				toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+			},
 			table: {
 				contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ]
 			}
 		} )
 		.then( editor => {
 			window.editors[ inspectorName ] = editor;
-			CKEditorInspector.attach( inspectorName, editor );
+			CKEditorInspector.attach( { [ inspectorName ]: editor } );
 
 			editor.model.document.on( 'change', () => {
 				printModelContents( editor );

+ 2 - 2
packages/ckeditor5-table/tests/manual/tableselection.md

@@ -5,5 +5,5 @@ Selecting table cells:
 1. It should be possible to select multiple table cells.
 2. Observe selection inn the below model representation - for a block selection the table cells should be selected.
 3. The editors are exposed as:
-    - `window.editor.content` and "content" editor in CKEditor inspector
-    - `window.editor.geometry` and "geometry" editor in CKEditor inspector
+    - `window.editors.content` and "content" editor in CKEditor inspector
+    - `window.editors.geometry` and "geometry" editor in CKEditor inspector

+ 4 - 2
packages/ckeditor5-table/tests/table.js

@@ -6,11 +6,13 @@
 import Table from '../src/table';
 import TableEditing from '../src/tableediting';
 import TableUI from '../src/tableui';
+import TableSelection from '../src/tableselection';
+import TableClipboard from '../src/tableclipboard';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 
 describe( 'Table', () => {
-	it( 'requires TableEditing, TableUI and Widget', () => {
-		expect( Table.requires ).to.deep.equal( [ TableEditing, TableUI, Widget ] );
+	it( 'requires TableEditing, TableUI, TableSelection, TableClipboard, and Widget', () => {
+		expect( Table.requires ).to.deep.equal( [ TableEditing, TableUI, TableSelection, TableClipboard, Widget ] );
 	} );
 
 	it( 'has proper name', () => {

+ 107 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbackgroundcolorcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellBackgroundColorCommand from '../../../src/tablecellproperties/commands/tablecellbackgroundcolorcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -84,6 +96,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( 'blue' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cell have the "backgroundColor" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "backgroundColor" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, backgroundColor: '#f00' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, backgroundColor: '#f00' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "backgroundColor" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, backgroundColor: '#f00' },
+								{ contents: '01', isSelected: true, backgroundColor: 'pink' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, backgroundColor: '#f00' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cell have the same "backgroundColor" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, backgroundColor: '#f00' },
+								{ contents: '01', isSelected: true, backgroundColor: '#f00' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, backgroundColor: '#f00' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( '#f00' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -147,6 +221,38 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "backgroundColor" attribute value of selected table cells', () => {
+						command.execute( { value: '#f00' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ { contents: '00', style: 'background-color:#f00;' }, '01' ],
+							[ '10', { contents: '11', style: 'background-color:#f00;' } ]
+						] ) );
+					} );
+
+					it( 'should remove "backgroundColor" from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, backgroundColor: '#f00' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, backgroundColor: '#f00' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 107 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbordercolorcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellBorderColorCommand from '../../../src/tablecellproperties/commands/tablecellbordercolorcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -109,6 +121,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( 'blue' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cells have the "borderColor" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "borderColor" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderColor: '#f00' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderColor: '#f00' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "borderColor" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderColor: '#f00' },
+								{ contents: '01', isSelected: true, borderColor: 'pink' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderColor: '#f00' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cells have the same "borderColor" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderColor: '#f00' },
+								{ contents: '01', isSelected: true, borderColor: '#f00' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderColor: '#f00' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( '#f00' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -172,6 +246,38 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "borderColor" attribute value of selected table cells', () => {
+						command.execute( { value: '#f00' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ { contents: '00', style: 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' }, '01' ],
+							[ '10', { contents: '11', style: 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' } ]
+						] ) );
+					} );
+
+					it( 'should remove "borderColor" from the selected table cell if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, borderColor: '#f00' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, borderColor: '#f00' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 113 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderstylecommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellBorderStyleCommand from '../../../src/tablecellproperties/commands/tablecellborderstylecommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -109,6 +121,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( 'ridge' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cells have the "borderStyle" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "borderStyle" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderStyle: 'solid' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderStyle: 'solid' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "borderStyle" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderStyle: 'solid' },
+								{ contents: '01', isSelected: true, borderStyle: 'ridge' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderStyle: 'solid' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cells have the same "borderStyle" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderStyle: 'solid' },
+								{ contents: '01', isSelected: true, borderStyle: 'solid' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderStyle: 'solid' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( 'solid' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -172,6 +246,44 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "borderStyle" attribute value of selected table cells', () => {
+						command.execute( { value: 'solid' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[
+								{ contents: '00', style: 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' },
+								'01'
+							],
+							[
+								'10',
+								{ contents: '11', style: 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' }
+							]
+						] ) );
+					} );
+
+					it( 'should remove "borderStyle" from selected table cells if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, borderStyle: 'solid' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, borderStyle: 'solid' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 113 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderwidthcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellBorderWidthCommand from '../../../src/tablecellproperties/commands/tablecellborderwidthcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -109,6 +121,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( '2em' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cells have the "borderWidth" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "borderWidth" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderWidth: '1px' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderWidth: '1px' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "borderWidth" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderWidth: '1px' },
+								{ contents: '01', isSelected: true, borderWidth: '20px' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderWidth: '1px' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cells have the same "borderWidth" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, borderWidth: '1px' },
+								{ contents: '01', isSelected: true, borderWidth: '1px' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, borderWidth: '1px' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( '1px' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -228,6 +302,44 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "borderWidth" attribute value of selected table cells', () => {
+						command.execute( { value: '1px' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[
+								{ contents: '00', style: 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' },
+								'01'
+							],
+							[
+								'10',
+								{ contents: '11', style: 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' }
+							]
+						] ) );
+					} );
+
+					it( 'should remove "borderWidth" from selected table cells if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, borderWidth: '1px' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, borderWidth: '1px' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 107 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellheightcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellHeightCommand from '../../../src/tablecellproperties/commands/tablecellheightcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -84,6 +96,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( '100px' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cell have the "height" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "height" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, height: '100px' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, height: '100px' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "height" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, height: '100px' },
+								{ contents: '01', isSelected: true, height: '23px' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, height: '100px' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cell have the same "height" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, height: '100px' },
+								{ contents: '01', isSelected: true, height: '100px' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, height: '100px' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -203,6 +277,38 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "height" attribute value of selected table cells', () => {
+						command.execute( { value: '100px' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ { contents: '00', style: 'height:100px;' }, '01' ],
+							[ '10', { contents: '11', style: 'height:100px;' } ]
+						] ) );
+					} );
+
+					it( 'should remove "height" from selected table cells if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, height: '100px' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, height: '100px' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 107 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellHorizontalAlignmentCommand from '../../../src/tablecellproperties/commands/tablecellhorizontalalignmentcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -84,6 +96,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( 'center' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cells have the "horizontalAlignment" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "horizontalAlignment" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, horizontalAlignment: 'center' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, horizontalAlignment: 'center' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "horizontalAlignment" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, horizontalAlignment: 'center' },
+								{ contents: '01', isSelected: true, horizontalAlignment: 'right' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, horizontalAlignment: 'center' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cells have the same "horizontalAlignment" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, horizontalAlignment: 'center' },
+								{ contents: '01', isSelected: true, horizontalAlignment: 'center' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, horizontalAlignment: 'center' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( 'center' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -147,6 +221,38 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "horizontalAlignment" attribute value of selected table cells', () => {
+						command.execute( { value: 'right' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ { contents: '00', style: 'text-align:right;' }, '01' ],
+							[ '10', { contents: '11', style: 'text-align:right;' } ]
+						] ) );
+					} );
+
+					it( 'should remove the "horizontalAlignment" attribute from selected table cells if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, horizontalAlignment: 'right' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, horizontalAlignment: 'right' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 107 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellpaddingcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellPaddingCommand from '../../../src/tablecellproperties/commands/tablecellpaddingcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -109,6 +121,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( '2em' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cells have the "padding" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "padding" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, padding: '2em' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, padding: '2em' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "padding" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, padding: '2em' },
+								{ contents: '01', isSelected: true, padding: '3em' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, padding: '2em' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cells have the same "padding" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, padding: '2em' },
+								{ contents: '01', isSelected: true, padding: '2em' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, padding: '2em' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( '2em' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -228,6 +302,38 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "padding" attribute value of selected table cells', () => {
+						command.execute( { value: '25px' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ { contents: '00', style: 'padding:25px;' }, '01' ],
+							[ '10', { contents: '11', style: 'padding:25px;' } ]
+						] ) );
+					} );
+
+					it( 'should remove "padding" from selected table cells if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, padding: '25px' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, padding: '25px' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 107 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellverticalalignmentcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellVerticalAlignmentCommand from '../../../src/tablecellproperties/commands/tablecellverticalalignmentcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -84,6 +96,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( 'bottom' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cells have the "verticalAlignment" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "verticalAlignment" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, verticalAlignment: 'bottom' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, verticalAlignment: 'bottom' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "verticalAlignment" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, verticalAlignment: 'bottom' },
+								{ contents: '01', isSelected: true, verticalAlignment: 'top' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, verticalAlignment: 'bottom' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cells have the same "verticalAlignment" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, verticalAlignment: 'bottom' },
+								{ contents: '01', isSelected: true, verticalAlignment: 'bottom' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, verticalAlignment: 'bottom' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( 'bottom' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -147,6 +221,38 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "verticalAlignment" attribute value of selected table cells', () => {
+						command.execute( { value: 'top' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ { contents: '00', style: 'vertical-align:top;' }, '01' ],
+							[ '10', { contents: '11', style: 'vertical-align:top;' } ]
+						] ) );
+					} );
+
+					it( 'should remove "verticalAlignment" from selected table cells if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, verticalAlignment: 'top' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, verticalAlignment: 'top' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 107 - 1
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellwidthcommand.js

@@ -8,9 +8,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import { assertTableCellStyle, modelTable, viewTable } from '../../_utils/utils';
 import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
 import TableCellWidthCommand from '../../../src/tablecellproperties/commands/tablecellwidthcommand';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'commands', () => {
@@ -54,6 +55,17 @@ describe( 'table cell properties', () => {
 						expect( command.isEnabled ).to.be.true;
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be true if the selection contains some table cells', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
 			} );
 
 			describe( 'value', () => {
@@ -84,6 +96,68 @@ describe( 'table cell properties', () => {
 						expect( command.value ).to.equal( '100px' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					it( 'should be undefined if no table cells have the "width" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if only some table cells have the "width" property', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, width: '100px' },
+								{ contents: '01', isSelected: true }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, width: '100px' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be undefined if one of selected table cells has a different "width" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, width: '100px' },
+								{ contents: '01', isSelected: true, width: '25px' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, width: '100px' }
+							]
+						] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if all table cells have the same "width" property value', () => {
+						setData( model, modelTable( [
+							[
+								{ contents: '00', isSelected: true, width: '100px' },
+								{ contents: '01', isSelected: true, width: '100px' }
+							],
+							[
+								'10',
+								{ contents: '11', isSelected: true, width: '100px' }
+							]
+						] ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
 			} );
 
 			describe( 'execute()', () => {
@@ -203,6 +277,38 @@ describe( 'table cell properties', () => {
 						assertTableCellStyle( editor, '' );
 					} );
 				} );
+
+				describe( 'multi-cell selection', () => {
+					beforeEach( () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true }, '01' ],
+							[ '10', { contents: '11', isSelected: true } ]
+						] ) );
+					} );
+
+					it( 'should set the "width" attribute value of selected table cells', () => {
+						command.execute( { value: '25px' } );
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ { contents: '00', style: 'width:25px;' }, '01' ],
+							[ '10', { contents: '11', style: 'width:25px;' } ]
+						] ) );
+					} );
+
+					it( 'should remove "width" from selected table cells if no value is passed', () => {
+						setData( model, modelTable( [
+							[ { contents: '00', isSelected: true, width: '25px' }, '01' ],
+							[ '10', { contents: '11', isSelected: true, width: '25px' } ]
+						] ) );
+
+						command.execute();
+
+						assertEqualMarkup( editor.getData(), viewTable( [
+							[ '00', '01' ],
+							[ '10', '11' ]
+						] ) );
+					} );
+				} );
 			} );
 		} );
 	} );

+ 15 - 1
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js

@@ -8,7 +8,7 @@
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
-import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getModelData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
@@ -21,6 +21,7 @@ import TableCellPropertiesEditing from '../../src/tablecellproperties/tablecellp
 import TableCellPropertiesUI from '../../src/tablecellproperties/tablecellpropertiesui';
 import TableCellPropertiesUIView from '../../src/tablecellproperties/ui/tablecellpropertiesview';
 import { defaultColors } from '../../src/ui/utils';
+import { modelTable } from '../_utils/utils';
 
 describe( 'table cell properties', () => {
 	describe( 'TableCellPropertiesUI', () => {
@@ -529,6 +530,19 @@ describe( 'table cell properties', () => {
 				expect( firstBatch ).to.not.equal( secondBatch );
 			} );
 
+			it( 'should show the ui for multi-cell selection', () => {
+				setData( editor.model, modelTable( [ [ '01', '02' ] ] ) );
+				editor.model.change( writer => {
+					writer.setSelection( [
+						writer.createRangeOn( editor.model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] ) ),
+						writer.createRangeOn( editor.model.document.getRoot().getNodeByPath( [ 0, 0, 1 ] ) )
+					], 0 );
+				} );
+
+				tableCellPropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tableCellPropertiesView );
+			} );
+
 			describe( 'initial data', () => {
 				it( 'should be set from the command values', () => {
 					editor.commands.get( 'tableCellBorderStyle' ).value = 'a';

+ 155 - 101
packages/ckeditor5-table/tests/tableclipboard.js

@@ -5,14 +5,13 @@
 
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 import TableEditing from '../src/tableediting';
 import { modelTable, viewTable } from './_utils/utils';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
-import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment';
-import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import TableClipboard from '../src/tableclipboard';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'table clipboard', () => {
 	let editor, model, modelRoot, tableSelection, viewDocument;
@@ -40,7 +39,7 @@ describe( 'table clipboard', () => {
 
 	describe( 'Clipboard integration', () => {
 		describe( 'copy', () => {
-			it( 'should to nothing for normal selection in table', () => {
+			it( 'should do nothing for normal selection in table', () => {
 				const dataTransferMock = createDataTransfer();
 				const spy = sinon.spy();
 
@@ -54,134 +53,139 @@ describe( 'table clipboard', () => {
 				sinon.assert.calledOnce( spy );
 			} );
 
-			it( 'should copy selected table cells as standalone table', done => {
-				const dataTransferMock = createDataTransfer();
+			it( 'should copy selected table cells as a standalone table', () => {
 				const preventDefaultSpy = sinon.spy();
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) );
-
-				viewDocument.on( 'clipboardOutput', ( evt, data ) => {
-					expect( preventDefaultSpy.calledOnce ).to.be.true;
-					expect( data.method ).to.equal( 'copy' );
-
-					expect( data.dataTransfer ).to.equal( dataTransferMock );
-
-					expect( data.content ).is.instanceOf( ViewDocumentFragment );
-					expect( stringifyView( data.content ) ).to.equal( viewTable( [
-						[ '01', '02' ],
-						[ '11', '12' ]
-					] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 2 ] )
+				);
 
-					done();
-				} );
-
-				viewDocument.fire( 'copy', {
-					dataTransfer: dataTransferMock,
+				const data = {
+					dataTransfer: createDataTransfer(),
 					preventDefault: preventDefaultSpy
-				} );
+				};
+				viewDocument.fire( 'copy', data );
+
+				sinon.assert.calledOnce( preventDefaultSpy );
+				expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( viewTable( [
+					[ '01', '02' ],
+					[ '11', '12' ]
+				] ) );
 			} );
 
-			it( 'should trim selected table to a selection rectangle (inner cell with colspan, no colspan after trim)', done => {
+			it( 'should trim selected table to a selection rectangle (inner cell with colspan, no colspan after trim)', () => {
 				setModelData( model, modelTable( [
 					[ '00[]', '01', '02' ],
 					[ '10', { contents: '11', colspan: 2 } ],
 					[ '20', '21', '22' ]
 				] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
+				assertClipboardContentOnMethod( 'copy', viewTable( [
 					[ '00', '01' ],
 					[ '10', '11' ],
 					[ '20', '21' ]
-				] ), done );
+				] ) );
 			} );
 
-			it( 'should trim selected table to a selection rectangle (inner cell with colspan, has colspan after trim)', done => {
+			it( 'should trim selected table to a selection rectangle (inner cell with colspan, has colspan after trim)', () => {
 				setModelData( model, modelTable( [
 					[ '00[]', '01', '02' ],
 					[ { contents: '10', colspan: 3 } ],
 					[ '20', '21', '22' ]
 				] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
+				assertClipboardContentOnMethod( 'copy', viewTable( [
 					[ '00', '01' ],
 					[ { contents: '10', colspan: 2 } ],
 					[ '20', '21' ]
-				] ), done );
+				] ) );
 			} );
 
-			it( 'should trim selected table to a selection rectangle (inner cell with rowspan, no colspan after trim)', done => {
+			it( 'should trim selected table to a selection rectangle (inner cell with rowspan, no colspan after trim)', () => {
 				setModelData( model, modelTable( [
 					[ '00[]', '01', '02' ],
 					[ '10', { contents: '11', rowspan: 2 }, '12' ],
 					[ '20', '21', '22' ]
 				] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 2 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
+				assertClipboardContentOnMethod( 'copy', viewTable( [
 					[ '00', '01', '02' ],
 					[ '10', '11', '12' ]
-				] ), done );
+				] ) );
 			} );
 
-			it( 'should trim selected table to a selection rectangle (inner cell with rowspan, has rowspan after trim)', done => {
+			it( 'should trim selected table to a selection rectangle (inner cell with rowspan, has rowspan after trim)', () => {
 				setModelData( model, modelTable( [
 					[ '00[]', { contents: '01', rowspan: 3 }, '02' ],
 					[ '10', '12' ],
 					[ '20', '22' ]
 				] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
+				assertClipboardContentOnMethod( 'copy', viewTable( [
 					[ '00', { contents: '01', rowspan: 2 }, '02' ],
 					[ '10', '12' ]
-				] ), done );
+				] ) );
 			} );
 
-			it( 'should prepend spanned columns with empty cells (outside cell with colspan)', done => {
+			it( 'should prepend spanned columns with empty cells (outside cell with colspan)', () => {
 				setModelData( model, modelTable( [
 					[ '00[]', '01', '02' ],
 					[ { contents: '10', colspan: 2 }, '12' ],
 					[ '20', '21', '22' ]
 				] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 2 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
+				assertClipboardContentOnMethod( 'copy', viewTable( [
 					[ '01', '02' ],
-					[ '', '12' ],
+					[ '&nbsp;', '12' ],
 					[ '21', '22' ]
-				] ), done );
+				] ) );
 			} );
 
-			it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', done => {
+			it( 'should prepend spanned columns with empty cells (outside cell with rowspan)', () => {
 				setModelData( model, modelTable( [
 					[ '00[]', { contents: '01', rowspan: 2 }, '02' ],
 					[ '10', '12' ],
 					[ '20', '21', '22' ]
 				] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 2 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
-					[ '10', '', '12' ],
+				assertClipboardContentOnMethod( 'copy', viewTable( [
+					[ '10', '&nbsp;', '12' ],
 					[ '20', '21', '22' ]
-				] ), done );
+				] ) );
 			} );
 
-			it( 'should fix selected table to a selection rectangle (hardcore case)', done => {
+			it( 'should fix selected table to a selection rectangle (hardcore case)', () => {
 				// This test check how previous simple rules run together (mixed prepending and trimming).
 				// In the example below a selection is set from cell "32" to "88"
 				//
@@ -219,20 +223,22 @@ describe( 'table clipboard', () => {
 					[ '80', '82', '83', '84', '85', '87', '88' ]
 				] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 7, 6 ] ) );
-
-				assertClipboardCopy( viewTable( [
-					[ '21', '', '23', '', '', { contents: '27', colspan: 2 } ],
-					[ '31', '', '33', '', '', '37', '37' ],
-					[ '', '', '', '', '', '47', '47' ],
-					[ '51', '52', '53', '', '', { contents: '57', rowspan: 3 }, '57' ],
-					[ { contents: '61', colspan: 3 }, '', '', '', '67' ],
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 7, 6 ] )
+				);
+
+				assertClipboardContentOnMethod( 'copy', viewTable( [
+					[ '21', '&nbsp;', '23', '&nbsp;', '&nbsp;', { contents: '27', colspan: 2 } ],
+					[ '31', '&nbsp;', '33', '&nbsp;', '&nbsp;', '37', '37' ],
+					[ '&nbsp;', '&nbsp;', '&nbsp;', '&nbsp;', '&nbsp;', '47', '47' ],
+					[ '51', '52', '53', '&nbsp;', '&nbsp;', { contents: '57', rowspan: 3 }, '57' ],
+					[ { contents: '61', colspan: 3 }, '&nbsp;', '&nbsp;', '&nbsp;', '67' ],
 					[ '71', '72', '73', '74', '75', '77' ]
-				] ), done );
+				] ) );
 			} );
 
-			it( 'should update table heading attributes (selection with headings)', done => {
+			it( 'should update table heading attributes (selection with headings)', () => {
 				setModelData( model, modelTable( [
 					[ '00', '01', '02', '03', '04' ],
 					[ '10', '11', '12', '13', '14' ],
@@ -241,17 +247,19 @@ describe( 'table clipboard', () => {
 					[ '40', '41', '42', '43', '44' ]
 				], { headingRows: 3, headingColumns: 2 } ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 3, 3 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 3, 3 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
+				assertClipboardContentOnMethod( 'copy', viewTable( [
 					[ '11', '12', '13' ],
 					[ '21', '22', '23' ],
 					[ { contents: '31', isHeading: true }, '32', '33' ] // TODO: bug in viewTable
-				], { headingRows: 2, headingColumns: 1 } ), done );
+				], { headingRows: 2, headingColumns: 1 } ) );
 			} );
 
-			it( 'should update table heading attributes (selection without headings)', done => {
+			it( 'should update table heading attributes (selection without headings)', () => {
 				setModelData( model, modelTable( [
 					[ '00', '01', '02', '03', '04' ],
 					[ '10', '11', '12', '13', '14' ],
@@ -260,63 +268,109 @@ describe( 'table clipboard', () => {
 					[ '40', '41', '42', '43', '44' ]
 				], { headingRows: 3, headingColumns: 2 } ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 3, 2 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 4, 4 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 3, 2 ] ),
+					modelRoot.getNodeByPath( [ 0, 4, 4 ] )
+				);
 
-				assertClipboardCopy( viewTable( [
+				assertClipboardContentOnMethod( 'copy', viewTable( [
 					[ '32', '33', '34' ],
 					[ '42', '43', '44' ]
-				] ), done );
+				] ) );
 			} );
 		} );
 
 		describe( 'cut', () => {
-			it( 'is disabled for multi-range selection over a table', () => {
+			it( 'should not block clipboardOutput if no multi-cell selection', () => {
+				setModelData( model, modelTable( [
+					[ '[00]', '01', '02' ],
+					[ '10', '11', '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+
 				const dataTransferMock = createDataTransfer();
-				const preventDefaultSpy = sinon.spy();
-				const spy = sinon.spy();
 
-				viewDocument.on( 'clipboardOutput', spy );
+				viewDocument.fire( 'cut', {
+					dataTransfer: dataTransferMock,
+					preventDefault: sinon.spy()
+				} );
+
+				expect( dataTransferMock.getData( 'text/html' ) ).to.equal( '00' );
+			} );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 2 ] ) );
+			it( 'should be preventable', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				viewDocument.on( 'clipboardOutput', evt => evt.stop(), { priority: 'high' } );
 
 				viewDocument.fire( 'cut', {
-					dataTransfer: dataTransferMock,
-					preventDefault: preventDefaultSpy
+					dataTransfer: createDataTransfer(),
+					preventDefault: sinon.spy()
 				} );
 
-				sinon.assert.notCalled( spy );
-				sinon.assert.calledOnce( preventDefaultSpy );
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ { contents: '00', isSelected: true }, { contents: '01', isSelected: true }, '02' ],
+					[ { contents: '10', isSelected: true }, { contents: '11', isSelected: true }, '12' ],
+					[ '20', '21', '22' ]
+				] ) );
 			} );
 
-			it( 'is not disabled normal selection over a table', () => {
-				const dataTransferMock = createDataTransfer();
+			it( 'is clears selected table cells', () => {
 				const spy = sinon.spy();
 
 				viewDocument.on( 'clipboardOutput', spy );
 
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
 				viewDocument.fire( 'cut', {
-					dataTransfer: dataTransferMock,
+					dataTransfer: createDataTransfer(),
 					preventDefault: sinon.spy()
 				} );
 
-				sinon.assert.calledOnce( spy );
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ '', '', '02' ],
+					[ '', '[]', '12' ],
+					[ '20', '21', '22' ]
+				] ) );
 			} );
-		} );
-	} );
 
-	function assertClipboardCopy( expectedViewTable, callback ) {
-		viewDocument.on( 'clipboardOutput', ( evt, data ) => {
-			expect( stringifyView( data.content ) ).to.equal( expectedViewTable );
+			it( 'should copy selected table cells as a standalone table', () => {
+				const preventDefaultSpy = sinon.spy();
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 2 ] )
+				);
+
+				const data = {
+					dataTransfer: createDataTransfer(),
+					preventDefault: preventDefaultSpy
+				};
+				viewDocument.fire( 'cut', data );
 
-			callback();
+				sinon.assert.calledOnce( preventDefaultSpy );
+				expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( viewTable( [
+					[ '01', '02' ],
+					[ '11', '12' ]
+				] ) );
+			} );
 		} );
+	} );
 
-		viewDocument.fire( 'copy', {
+	function assertClipboardContentOnMethod( method, expectedViewTable ) {
+		const data = {
 			dataTransfer: createDataTransfer(),
 			preventDefault: sinon.spy()
-		} );
+		};
+		viewDocument.fire( method, data );
+
+		expect( data.dataTransfer.getData( 'text/html' ) ).to.equal( expectedViewTable );
 	}
 
 	function createDataTransfer() {

+ 231 - 0
packages/ckeditor5-table/tests/tableselection-integration.js

@@ -0,0 +1,231 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import TableEditing from '../src/tableediting';
+import TableSelection from '../src/tableselection';
+import { modelTable } from './_utils/utils';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
+import Delete from '@ckeditor/ckeditor5-typing/src/delete';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import Input from '@ckeditor/ckeditor5-typing/src/input';
+import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
+
+describe( 'table selection', () => {
+	let editor, model, tableSelection, modelRoot, element, viewDocument;
+
+	describe( 'TableSelection - input integration', () => {
+		afterEach( async () => {
+			element.remove();
+			await editor.destroy();
+		} );
+
+		describe( 'on delete', () => {
+			beforeEach( async () => {
+				await setupEditor( [ Delete ] );
+			} );
+
+			it( 'should clear contents of the selected table cells and put selection in last cell on backward delete', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				const domEventData = new DomEventData( viewDocument, {
+					preventDefault: sinon.spy()
+				}, {
+					direction: 'backward',
+					unit: 'character',
+					sequence: 1
+				} );
+				viewDocument.fire( 'delete', domEventData );
+
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ '', '', '13' ],
+					[ '', '[]', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+			} );
+
+			it( 'should clear contents of the selected table cells and put selection in last cell on forward delete', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				const domEventData = new DomEventData( viewDocument, {
+					preventDefault: sinon.spy()
+				}, {
+					direction: 'forward',
+					unit: 'character',
+					sequence: 1
+				} );
+				viewDocument.fire( 'delete', domEventData );
+
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ '[]', '', '13' ],
+					[ '', '', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+			} );
+
+			it( 'should not interfere with default key handler if no table selection', () => {
+				setModelData( model, modelTable( [
+					[ '11[]', '12', '13' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+
+				const domEventData = new DomEventData( viewDocument, {
+					preventDefault: sinon.spy()
+				}, {
+					direction: 'backward',
+					unit: 'character',
+					sequence: 1
+				} );
+				viewDocument.fire( 'delete', domEventData );
+
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ '1[]', '12', '13' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+			} );
+
+			it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete backwards)', () => {
+				const selection = model.createSelection( [
+					model.createRange(
+						model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
+						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
+					),
+					model.createRange(
+						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
+						model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
+					)
+				] );
+
+				model.change( writer => {
+					model.deleteContent( selection );
+					writer.setSelection( selection );
+				} );
+
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ '', '[]', '13' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+			} );
+
+			it( 'should work with any arbitrary selection passed to Model#deleteContent() (delete forwards)', () => {
+				const selection = model.createSelection( [
+					model.createRange(
+						model.createPositionFromPath( modelRoot, [ 0, 0, 0 ] ),
+						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] )
+					),
+					model.createRange(
+						model.createPositionFromPath( modelRoot, [ 0, 0, 1 ] ),
+						model.createPositionFromPath( modelRoot, [ 0, 0, 2 ] )
+					)
+				] );
+
+				model.change( writer => {
+					model.deleteContent( selection, {
+						direction: 'forward'
+					} );
+					writer.setSelection( selection );
+				} );
+
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ '[]', '', '13' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+			} );
+		} );
+
+		describe( 'on user input', () => {
+			beforeEach( async () => {
+				await setupEditor( [ Input ] );
+			} );
+
+			it( 'should clear contents of the selected table cells and put selection in last cell on user input', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } );
+
+				// Mutate at the place where the document selection was put; it's more realistic
+				// than mutating at some arbitrary position.
+				const placeOfMutation = viewDocument.selection.getFirstRange().start.parent;
+
+				viewDocument.fire( 'mutations', [
+					{
+						type: 'children',
+						oldChildren: [],
+						newChildren: [ new ViewText( viewDocument, 'x' ) ],
+						node: placeOfMutation
+					}
+				] );
+
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ '', '', '13' ],
+					[ '', 'x[]', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+			} );
+
+			it( 'should not interfere with default key handler if no table selection', () => {
+				viewDocument.fire( 'keydown', { keyCode: getCode( 'x' ) } );
+
+				// Mutate at the place where the document selection was put; it's more realistic
+				// than mutating at some arbitrary position.
+				const placeOfMutation = viewDocument.selection.getFirstRange().start.parent;
+
+				viewDocument.fire( 'mutations', [
+					{
+						type: 'children',
+						oldChildren: [],
+						newChildren: [ new ViewText( viewDocument, 'x' ) ],
+						node: placeOfMutation
+					}
+				] );
+
+				assertEqualMarkup( getModelData( model ), modelTable( [
+					[ 'x[]11', '12', '13' ],
+					[ '21', '22', '23' ],
+					[ '31', '32', '33' ]
+				] ) );
+			} );
+		} );
+	} );
+
+	async function setupEditor( plugins ) {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		editor = await ClassicTestEditor.create( element, {
+			plugins: [ TableEditing, TableSelection, Paragraph, ...plugins ]
+		} );
+
+		model = editor.model;
+		modelRoot = model.document.getRoot();
+		viewDocument = editor.editing.view.document;
+		tableSelection = editor.plugins.get( TableSelection );
+
+		setModelData( model, modelTable( [
+			[ '[]11', '12', '13' ],
+			[ '21', '22', '23' ],
+			[ '31', '32', '33' ]
+		] ) );
+	}
+} );

+ 44 - 237
packages/ckeditor5-table/tests/tableselection.js

@@ -9,9 +9,10 @@ import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-util
 
 import TableEditing from '../src/tableediting';
 import TableSelection from '../src/tableselection';
-import { assertSelectedCells, modelTable, viewTable } from './_utils/utils';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+// import { assertSelectedCells, modelTable, viewTable } from './_utils/utils';
+import { modelTable } from './_utils/utils';
+// import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+// import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import DocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
 
 describe( 'table selection', () => {
@@ -38,224 +39,49 @@ describe( 'table selection', () => {
 	} );
 
 	describe( 'TableSelection', () => {
-		describe( 'startSelectingFrom()', () => {
-			it( 'should not change model selection', () => {
-				const spy = sinon.spy();
+		describe( 'selection by shift+click', () => {
+			it( 'should...', () => {
+				// tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				// tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
 
-				model.document.selection.on( 'change', spy );
+				// tableSelection.stopSelection();
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-
-				sinon.assert.notCalled( spy );
-			} );
-		} );
-
-		describe( 'setSelectingTo()', () => {
-			it( 'should set model selection on passed cell if startSelectingFrom() was not used', () => {
-				const spy = sinon.spy();
-
-				model.document.selection.on( 'change', spy );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				sinon.assert.calledOnce( spy );
-
-				assertSelectedCells( model, [
-					[ 1, 1, 0 ],
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ]
-				] );
-			} );
-
-			it( 'should change model selection if valid selection will be set', () => {
-				const spy = sinon.spy();
-
-				model.document.selection.on( 'change', spy );
-
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				sinon.assert.calledOnce( spy );
-			} );
-
-			it( 'should not change model selection if passed table cell is from other table then start cell', () => {
-				setModelData( model,
-					modelTable( [
-						[ '11[]', '12', '13' ],
-						[ '21', '22', '23' ],
-						[ '31', '32', '33' ]
-					] ) +
-					modelTable( [
-						[ 'a', 'b' ],
-						[ 'c', 'd' ]
-					] )
-				);
-
-				const spy = sinon.spy();
-
-				model.document.selection.on( 'change', spy );
-
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 1, 0, 1 ] ) );
-
-				sinon.assert.notCalled( spy );
-			} );
-
-			it( 'should select two table cells', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				assertSelectedCells( model, [
-					[ 1, 1, 0 ],
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ]
-				] );
-			} );
-
-			it( 'should select four table cells for diagonal selection', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
-
-				assertSelectedCells( model, [
-					[ 1, 1, 0 ],
-					[ 1, 1, 0 ],
-					[ 0, 0, 0 ]
-				] );
-			} );
-
-			it( 'should select row table cells', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 2 ] ) );
-
-				assertSelectedCells( model, [
-					[ 1, 1, 1 ],
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ]
-				] );
-			} );
-
-			it( 'should select column table cells', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
-
-				assertSelectedCells( model, [
-					[ 0, 1, 0 ],
-					[ 0, 1, 0 ],
-					[ 0, 1, 0 ]
-				] );
-			} );
-
-			it( 'should create proper selection on consecutive changes', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 1 ] ) );
-
-				assertSelectedCells( model, [
-					[ 0, 0, 0 ],
-					[ 0, 1, 0 ],
-					[ 0, 1, 0 ]
-				] );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				assertSelectedCells( model, [
-					[ 0, 1, 0 ],
-					[ 0, 1, 0 ],
-					[ 0, 0, 0 ]
-				] );
-
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 2, 2 ] ) );
-
-				assertSelectedCells( model, [
-					[ 0, 0, 0 ],
-					[ 0, 1, 1 ],
-					[ 0, 1, 1 ]
-				] );
+				// assertSelectedCells( model, [
+				// 	[ 1, 1, 0 ],
+				// 	[ 0, 0, 0 ],
+				// 	[ 0, 0, 0 ]
+				// ] );
 			} );
 		} );
 
-		describe( 'stopSelection()', () => {
-			it( 'should not clear currently selected cells if not cell was passed', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				tableSelection.stopSelection();
-
-				assertSelectedCells( model, [
-					[ 1, 1, 0 ],
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ]
-				] );
-			} );
-
-			it( 'should change model selection if cell was passed', () => {
-				const spy = sinon.spy();
+		describe( 'selection by mouse drag', () => {
+			it( 'should...', () => {
+				// tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				// tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
 
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
+				// tableSelection.stopSelection();
 
-				model.document.selection.on( 'change', spy );
-				tableSelection.stopSelection( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				sinon.assert.calledOnce( spy );
-			} );
-
-			it( 'should extend selection to passed table cell', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-
-				tableSelection.stopSelection( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				assertSelectedCells( model, [
-					[ 1, 1, 0 ],
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ]
-				] );
+				// assertSelectedCells( model, [
+				// 	[ 1, 1, 0 ],
+				// 	[ 0, 0, 0 ],
+				// 	[ 0, 0, 0 ]
+				// ] );
 			} );
 		} );
 
-		describe( 'clearSelection()', () => {
-			it( 'should not change model selection', () => {
-				const spy = sinon.spy();
-
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				model.document.selection.on( 'change', spy );
-
-				tableSelection.clearSelection();
-
-				sinon.assert.notCalled( spy );
-			} );
-
-			it( 'should not reset model selections', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				tableSelection.clearSelection();
-
-				assertSelectedCells( model, [
-					[ 1, 1, 0 ],
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ]
-				] );
-			} );
-		} );
-
-		describe( '* getSelectedTableCells()', () => {
+		describe( 'getSelectedTableCells()', () => {
 			it( 'should return nothing if selection is not started', () => {
-				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [] );
+				expect( tableSelection.getSelectedTableCells() ).to.be.null;
 			} );
 
 			it( 'should return two table cells', () => {
 				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
 				const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
 
-				tableSelection.startSelectingFrom( firstCell );
-				tableSelection.setSelectingTo( lastCell );
+				tableSelection._setCellSelection(
+					firstCell,
+					lastCell
+				);
 
 				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
 					firstCell, lastCell
@@ -266,8 +92,10 @@ describe( 'table selection', () => {
 				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
 				const lastCell = modelRoot.getNodeByPath( [ 0, 1, 1 ] );
 
-				tableSelection.startSelectingFrom( firstCell );
-				tableSelection.setSelectingTo( lastCell );
+				tableSelection._setCellSelection(
+					firstCell,
+					lastCell
+				);
 
 				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
 					firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), modelRoot.getNodeByPath( [ 0, 1, 0 ] ), lastCell
@@ -278,8 +106,10 @@ describe( 'table selection', () => {
 				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
 				const lastCell = modelRoot.getNodeByPath( [ 0, 0, 2 ] );
 
-				tableSelection.startSelectingFrom( firstCell );
-				tableSelection.setSelectingTo( lastCell );
+				tableSelection._setCellSelection(
+					firstCell,
+					lastCell
+				);
 
 				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
 					firstCell, modelRoot.getNodeByPath( [ 0, 0, 1 ] ), lastCell
@@ -290,8 +120,7 @@ describe( 'table selection', () => {
 				const firstCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
 				const lastCell = modelRoot.getNodeByPath( [ 0, 2, 1 ] );
 
-				tableSelection.startSelectingFrom( firstCell );
-				tableSelection.setSelectingTo( lastCell );
+				tableSelection._setCellSelection( firstCell, lastCell );
 
 				expect( Array.from( tableSelection.getSelectedTableCells() ) ).to.deep.equal( [
 					firstCell, modelRoot.getNodeByPath( [ 0, 1, 1 ] ), lastCell
@@ -301,39 +130,17 @@ describe( 'table selection', () => {
 
 		describe( 'getSelectionAsFragment()', () => {
 			it( 'should return undefined if no table cells are selected', () => {
-				expect( tableSelection.getSelectionAsFragment() ).to.be.undefined;
+				expect( tableSelection.getSelectionAsFragment() ).to.be.null;
 			} );
 
 			it( 'should return document fragment for selected table cells', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 1, 1 ] ) );
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
 
 				expect( tableSelection.getSelectionAsFragment() ).to.be.instanceOf( DocumentFragment );
 			} );
 		} );
-
-		describe( 'behavior', () => {
-			it( 'should clear selection on external changes', () => {
-				tableSelection.startSelectingFrom( modelRoot.getNodeByPath( [ 0, 0, 0 ] ) );
-				tableSelection.setSelectingTo( modelRoot.getNodeByPath( [ 0, 0, 1 ] ) );
-
-				editor.model.change( writer => {
-					writer.setSelection( modelRoot.getNodeByPath( [ 0, 0, 0, 0 ] ), 0 );
-				} );
-
-				assertSelectedCells( model, [
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ],
-					[ 0, 0, 0 ]
-				] );
-
-				expect( editor.editing.view.document.selection.isFake ).to.be.false;
-				assertEqualMarkup( getViewData( editor.editing.view ), viewTable( [
-					[ '{}11', '12', '13' ],
-					[ '21', '22', '23' ],
-					[ '31', '32', '33' ]
-				], { asWidget: true } ) );
-			} );
-		} );
 	} );
 } );

+ 0 - 485
packages/ckeditor5-table/tests/tableselection/mouseselectionhandler.js

@@ -1,485 +0,0 @@
-/**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
-
-import TableEditing from '../../src/tableediting';
-import TableSelection from '../../src/tableselection';
-import { assertSelectedCells, modelTable, viewTable } from '../_utils/utils';
-
-describe( 'table selection', () => {
-	let editor, model, view, viewDoc;
-
-	beforeEach( async () => {
-		editor = await VirtualTestEditor.create( {
-			plugins: [ TableEditing, TableSelection, Paragraph ]
-		} );
-
-		model = editor.model;
-		view = editor.editing.view;
-		viewDoc = view.document;
-
-		setModelData( model, modelTable( [
-			[ '11[]', '12', '13' ],
-			[ '21', '22', '23' ],
-			[ '31', '32', '33' ]
-		] ) );
-	} );
-
-	afterEach( async () => {
-		await editor.destroy();
-	} );
-
-	describe( 'MouseSelectionHandler', () => {
-		it( 'should not start table selection when mouse move is inside one table cell', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			movePressedMouseOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-		} );
-
-		it( 'should start table selection when mouse move expands over two cells', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			movePressedMouseOver( getTableCell( '01' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 0, 0 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[ '10', '11' ]
-			], { asWidget: true } ) );
-		} );
-
-		it( 'should select rectangular table cells when mouse moved to diagonal cell (up -> down)', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			movePressedMouseOver( getTableCell( '11' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 1, 1 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[
-					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true }
-				]
-			], { asWidget: true } ) );
-		} );
-
-		it( 'should select rectangular table cells when mouse moved to diagonal cell (down -> up)', () => {
-			setModelData( model, modelTable( [
-				[ '00', '01' ],
-				[ '10', '[]11' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '11' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '00', '01' ],
-				[ '10', '[]11' ]
-			] ) );
-
-			movePressedMouseOver( getTableCell( '00' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 1, 1 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[
-					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true }
-				]
-			], { asWidget: true } ) );
-		} );
-
-		it( 'should update view selection after changing selection rect', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01', '02' ],
-				[ '10', '11', '12' ],
-				[ '20', '21', '22' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-			movePressedMouseOver( getTableCell( '22' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1, 1 ],
-				[ 1, 1, 1 ],
-				[ 1, 1, 1 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '02', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[
-					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '12', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[
-					{ contents: '20', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '21', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '22', class: 'ck-editor__editable_selected', isSelected: true }
-				]
-			], { asWidget: true } ) );
-
-			movePressedMouseOver( getTableCell( '11' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1, 0 ],
-				[ 1, 1, 0 ],
-				[ 0, 0, 0 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true },
-					'02'
-				],
-				[
-					{ contents: '10', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '11', class: 'ck-editor__editable_selected', isSelected: true },
-					'12'
-				],
-				[
-					'20',
-					'21',
-					'22'
-				]
-			], { asWidget: true } ) );
-		} );
-
-		it( 'should stop selecting after "mouseup" event', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			movePressedMouseOver( getTableCell( '01' ) );
-			releaseMouseButtonOver( getTableCell( '01' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 0, 0 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[ '10', '11' ]
-			], { asWidget: true } ) );
-
-			moveReleasedMouseOver( getTableCell( '11' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 0, 0 ]
-			] );
-		} );
-
-		it( 'should do nothing on "mouseup" event', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			releaseMouseButtonOver( getTableCell( '01' ) );
-
-			assertSelectedCells( model, [
-				[ 0, 0 ],
-				[ 0, 0 ]
-			] );
-		} );
-
-		it( 'should stop selection mode on "mouseleve" event if next "mousemove" has no button pressed', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			movePressedMouseOver( getTableCell( '01' ) );
-			makeMouseLeave();
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 0, 0 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[ '10', '11' ]
-			], { asWidget: true } ) );
-
-			moveReleasedMouseOver( getTableCell( '11' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 0, 0 ]
-			] );
-		} );
-
-		it( 'should continue selection mode on "mouseleve" and "mousemove" if mouse button is pressed', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			movePressedMouseOver( getTableCell( '01' ) );
-			makeMouseLeave();
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 0, 0 ]
-			] );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[
-					{ contents: '00', class: 'ck-editor__editable_selected', isSelected: true },
-					{ contents: '01', class: 'ck-editor__editable_selected', isSelected: true }
-				],
-				[ '10', '11' ]
-			], { asWidget: true } ) );
-
-			movePressedMouseOver( getTableCell( '11' ) );
-
-			assertSelectedCells( model, [
-				[ 1, 1 ],
-				[ 1, 1 ]
-			] );
-		} );
-
-		it( 'should do nothing on "mouseleve" event', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			makeMouseLeave();
-
-			assertSelectedCells( model, [
-				[ 0, 0 ],
-				[ 0, 0 ]
-			] );
-		} );
-
-		it( 'should do nothing on "mousedown" event over ui element (click on selection handle)', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			const uiElement = viewDoc.getRoot()
-				.getChild( 0 )
-				.getChild( 0 ); // selection handler;
-
-			fireEvent( view, 'mousedown', addTarget( uiElement ), mouseButtonPressed );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-		} );
-
-		it( 'should do nothing on "mousemove" event over ui element (click on selection handle)', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-
-			const uiElement = viewDoc.getRoot()
-				.getChild( 0 )
-				.getChild( 0 ); // selection handler;
-
-			fireEvent( view, 'mousemove', addTarget( uiElement ), mouseButtonPressed );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) );
-		} );
-
-		it( 'should clear view table selection after mouse click outside table', () => {
-			setModelData( model, modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) + '<paragraph>foo</paragraph>' );
-
-			pressMouseButtonOver( getTableCell( '00' ) );
-
-			assertEqualMarkup( getModelData( model ), modelTable( [
-				[ '[]00', '01' ],
-				[ '10', '11' ]
-			] ) + '<paragraph>foo</paragraph>' );
-
-			movePressedMouseOver( getTableCell( '01' ) );
-
-			const paragraph = viewDoc.getRoot().getChild( 1 );
-
-			fireEvent( view, 'mousemove', addTarget( paragraph ) );
-			fireEvent( view, 'mousedown', addTarget( paragraph ) );
-			fireEvent( view, 'mouseup', addTarget( paragraph ) );
-
-			// The click in the DOM would trigger selection change and it will set the selection:
-			model.change( writer => {
-				writer.setSelection( writer.createRange( writer.createPositionAt( model.document.getRoot().getChild( 1 ), 0 ) ) );
-			} );
-
-			assertEqualMarkup( getViewData( view ), viewTable( [
-				[ '00', '01' ],
-				[ '10', '11' ]
-			], { asWidget: true } ) + '<p>{}foo</p>' );
-		} );
-	} );
-
-	function getTableCell( data ) {
-		for ( const value of view.createRangeIn( viewDoc.getRoot() ) ) {
-			if ( value.type === 'text' && value.item.data === data ) {
-				return value.item.parent.parent;
-			}
-		}
-	}
-
-	function makeMouseLeave() {
-		fireEvent( view, 'mouseleave' );
-	}
-
-	function pressMouseButtonOver( target ) {
-		fireEvent( view, 'mousedown', addTarget( target ), mouseButtonPressed );
-	}
-
-	function movePressedMouseOver( target ) {
-		moveMouseOver( target, mouseButtonPressed );
-	}
-
-	function moveReleasedMouseOver( target ) {
-		moveMouseOver( target, mouseButtonReleased );
-	}
-
-	function moveMouseOver( target, ...decorators ) {
-		fireEvent( view, 'mousemove', addTarget( target ), ...decorators );
-	}
-
-	function releaseMouseButtonOver( target ) {
-		fireEvent( view, 'mouseup', addTarget( target ), mouseButtonReleased );
-	}
-
-	function addTarget( target ) {
-		return domEventData => {
-			domEventData.target = target;
-		};
-	}
-
-	function mouseButtonPressed( domEventData ) {
-		domEventData.domEvent.buttons = 1;
-	}
-
-	function mouseButtonReleased( domEventData ) {
-		domEventData.domEvent.buttons = 0;
-	}
-
-	function fireEvent( view, eventName, ...decorators ) {
-		const domEvtDataStub = {
-			domEvent: {
-				buttons: 0
-			},
-			target: undefined,
-			preventDefault: sinon.spy(),
-			stopPropagation: sinon.spy()
-		};
-
-		for ( const decorator of decorators ) {
-			decorator( domEvtDataStub );
-		}
-
-		viewDoc.fire( eventName, domEvtDataStub );
-	}
-} );

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

@@ -26,7 +26,7 @@
 		& th {
 			min-width: 2em;
 			padding: .4em;
-			border-color: hsl(0, 0%, 85%);
+			border-color: hsl(0, 0%, 75%);
 		}
 
 		& th {

+ 0 - 7
packages/ckeditor5-table/theme/tableselection.css

@@ -8,10 +8,3 @@
  * it acts as a message to the builder telling that it should look for the corresponding styles
  * **in the theme** when compiling the editor.
  */
-
-.ck.ck-editor__editable .table table {
-	& td.ck-editor__editable_selected,
-	& th.ck-editor__editable_selected {
-		box-shadow: inset 0 0 0 1px var(--ck-color-focus-border);
-	}
-}