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

Merge branch 'master' into i/6546

Maciej Gołaszewski 5 лет назад
Родитель
Сommit
c2269321cb

+ 1 - 1
packages/ckeditor5-table/.travis.yml

@@ -9,7 +9,7 @@ services:
 cache:
 cache:
   yarn: true
   yarn: true
 node_js:
 node_js:
-- '8'
+- '10'
 branches:
 branches:
   except:
   except:
   - stable
   - stable

+ 10 - 2
packages/ckeditor5-table/docs/features/table.md

@@ -335,7 +335,7 @@ The table plugins register the following UI components:
 			<td>The <code>'tableRow'</code> dropdown</td>
 			<td>The <code>'tableRow'</code> dropdown</td>
 		</tr>
 		</tr>
 		<tr>
 		<tr>
-			<td>The <code>'mergeTableCells'</code> dropdown</td>
+			<td>The <code>'mergeTableCells'</code> split button</td>
 		</tr>
 		</tr>
 		<tr>
 		<tr>
 			<td>The <code>'tableProperties'</code> button</td>
 			<td>The <code>'tableProperties'</code> button</td>
@@ -351,7 +351,7 @@ The table plugins register the following UI components:
 #### Toolbars
 #### Toolbars
 
 
 The {@link module:table/tabletoolbar~TableToolbar} plugin introduces two balloon toolbars for tables.
 The {@link module:table/tabletoolbar~TableToolbar} plugin introduces two balloon toolbars for tables.
-* The content toolbar shows up when a table cell is selected and it is anchored to the table. It is possible to {@link module:table/table~TableConfig#contentToolbar configure} its content. Normally, the toolbar contains the table-related tools such as `'tableColumn'`, `'tableRow'`, and `'mergeTableCells'` dropdowns.
+* The content toolbar shows up when a table cell is selected and it is anchored to the table. It is possible to {@link module:table/table~TableConfig#contentToolbar configure} its content. Normally, the toolbar contains the table-related tools such as `'tableColumn'` and `'tableRow'` dropdowns and `'mergeTableCells'` split button.
 * The table toolbar shows up when the whole table is selected, for instance using the widget handler. It is possible to {@link module:table/table~TableConfig#tableToolbar configure} its content.
 * The table toolbar shows up when the whole table is selected, for instance using the widget handler. It is possible to {@link module:table/table~TableConfig#tableToolbar configure} its content.
 
 
 ### Editor commands
 ### Editor commands
@@ -394,6 +394,14 @@ The {@link module:table/tabletoolbar~TableToolbar} plugin introduces two balloon
 			<td><code>'removeTableRow'</code></td>
 			<td><code>'removeTableRow'</code></td>
 			<td>{@link module:table/commands/removerowcommand~RemoveRowCommand}</td>
 			<td>{@link module:table/commands/removerowcommand~RemoveRowCommand}</td>
 		</tr>
 		</tr>
+		<tr>
+			<td><code>'selectTableColumn'</code></td>
+			<td>{@link module:table/commands/selectcolumncommand~SelectColumnCommand}</td>
+		</tr>
+		<tr>
+			<td><code>'selectTableRow'</code></td>
+			<td>{@link module:table/commands/selectrowcommand~SelectRowCommand}</td>
+		</tr>
 		<tr>
 		<tr>
 			<td><code>'setTableColumnHeader'</code></td>
 			<td><code>'setTableColumnHeader'</code></td>
 			<td>{@link module:table/commands/setheadercolumncommand~SetHeaderColumnCommand}</td>
 			<td>{@link module:table/commands/setheadercolumncommand~SetHeaderColumnCommand}</td>

+ 2 - 0
packages/ckeditor5-table/lang/contexts.json

@@ -4,11 +4,13 @@
 	"Insert column left": "Label for the insert table column to the left of the current one button.",
 	"Insert column left": "Label for the insert table column to the left of the current one button.",
 	"Insert column right": "Label for the insert table column to the right of the current one button.",
 	"Insert column right": "Label for the insert table column to the right of the current one button.",
 	"Delete column": "Label for the delete table column button.",
 	"Delete column": "Label for the delete table column button.",
+	"Select column": "Label for the select table entire column button.",
 	"Column": "Label for the table column dropdown button.",
 	"Column": "Label for the table column dropdown button.",
 	"Header row": "Label for the set/unset table header row button.",
 	"Header row": "Label for the set/unset table header row button.",
 	"Insert row below": "Label for the insert row below button.",
 	"Insert row below": "Label for the insert row below button.",
 	"Insert row above": "Label for the insert row above button.",
 	"Insert row above": "Label for the insert row above button.",
 	"Delete row": "Label for the delete table row button.",
 	"Delete row": "Label for the delete table row button.",
+	"Select row": "Label for the select table entire row button.",
 	"Row": "Label for the table row dropdown button.",
 	"Row": "Label for the table row dropdown button.",
 	"Merge cell up": "Label for the merge table cell up button.",
 	"Merge cell up": "Label for the merge table cell up button.",
 	"Merge cell right": "Label for the merge table cell right button.",
 	"Merge cell right": "Label for the merge table cell right button.",

+ 65 - 0
packages/ckeditor5-table/src/commands/selectcolumncommand.js

@@ -0,0 +1,65 @@
+/**
+ * @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/commands/selectcolumncommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+import TableWalker from '../tablewalker';
+import { findAncestor } from './utils';
+import { getSelectionAffectedTableCells } from '../utils';
+
+/**
+ * The select column command.
+ *
+ * The command is registered by {@link module:table/tableediting~TableEditing} as the `'selectTableColumn'` editor command.
+ *
+ * To select the columns containing the selected cells, execute the command:
+ *
+ *		editor.execute( 'selectTableColumn' );
+ *
+ * @extends module:core/command~Command
+ */
+export default class SelectColumnCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const selectedCells = getSelectionAffectedTableCells( this.editor.model.document.selection );
+
+		this.isEnabled = selectedCells.length > 0;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute() {
+		const model = this.editor.model;
+		const referenceCells = getSelectionAffectedTableCells( model.document.selection );
+		const firstCell = referenceCells[ 0 ];
+		const lastCell = referenceCells.pop();
+
+		const tableUtils = this.editor.plugins.get( 'TableUtils' );
+		const startLocation = tableUtils.getCellLocation( firstCell );
+		const endLocation = tableUtils.getCellLocation( lastCell );
+
+		const startColumn = Math.min( startLocation.column, endLocation.column );
+		const endColumn = Math.max( startLocation.column, endLocation.column );
+
+		const rangesToSelect = [];
+
+		for ( const cellInfo of new TableWalker( findAncestor( 'table', firstCell ) ) ) {
+			if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
+				rangesToSelect.push( model.createRangeOn( cellInfo.cell ) );
+			}
+		}
+
+		model.change( writer => {
+			writer.setSelection( rangesToSelect );
+		} );
+	}
+}

+ 57 - 0
packages/ckeditor5-table/src/commands/selectrowcommand.js

@@ -0,0 +1,57 @@
+/**
+ * @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/commands/selectrowcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+import { findAncestor } from './utils';
+import { getRowIndexes, getSelectionAffectedTableCells } from '../utils';
+
+/**
+ * The select row command.
+ *
+ * The command is registered by {@link module:table/tableediting~TableEditing} as the `'selectTableRow'` editor command.
+ *
+ * To select the rows containing the selected cells, execute the command:
+ *
+ *		editor.execute( 'selectTableRow' );
+ *
+ * @extends module:core/command~Command
+ */
+export default class SelectRowCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const selectedCells = getSelectionAffectedTableCells( this.editor.model.document.selection );
+
+		this.isEnabled = selectedCells.length > 0;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute() {
+		const model = this.editor.model;
+		const referenceCells = getSelectionAffectedTableCells( model.document.selection );
+		const rowIndexes = getRowIndexes( referenceCells );
+
+		const table = findAncestor( 'table', referenceCells[ 0 ] );
+		const rangesToSelect = [];
+
+		for ( let rowIndex = rowIndexes.first; rowIndex <= rowIndexes.last; rowIndex++ ) {
+			for ( const cell of table.getChild( rowIndex ).getChildren() ) {
+				rangesToSelect.push( model.createRangeOn( cell ) );
+			}
+		}
+
+		model.change( writer => {
+			writer.setSelection( rangesToSelect );
+		} );
+	}
+}

+ 5 - 0
packages/ckeditor5-table/src/tableediting.js

@@ -29,6 +29,8 @@ import RemoveColumnCommand from './commands/removecolumncommand';
 import SetHeaderRowCommand from './commands/setheaderrowcommand';
 import SetHeaderRowCommand from './commands/setheaderrowcommand';
 import SetHeaderColumnCommand from './commands/setheadercolumncommand';
 import SetHeaderColumnCommand from './commands/setheadercolumncommand';
 import MergeCellsCommand from './commands/mergecellscommand';
 import MergeCellsCommand from './commands/mergecellscommand';
+import SelectRowCommand from './commands/selectrowcommand';
+import SelectColumnCommand from './commands/selectcolumncommand';
 import { getTableCellsContainingSelection } from './utils';
 import { getTableCellsContainingSelection } from './utils';
 import TableUtils from '../src/tableutils';
 import TableUtils from '../src/tableutils';
 
 
@@ -142,6 +144,9 @@ export default class TableEditing extends Plugin {
 		editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) );
 		editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) );
 		editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) );
 		editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) );
 
 
+		editor.commands.add( 'selectTableRow', new SelectRowCommand( editor ) );
+		editor.commands.add( 'selectTableColumn', new SelectColumnCommand( editor ) );
+
 		injectTableLayoutPostFixer( model );
 		injectTableLayoutPostFixer( model );
 		injectTableCellRefreshPostFixer( model );
 		injectTableCellRefreshPostFixer( model );
 		injectTableCellParagraphPostFixer( model );
 		injectTableCellParagraphPostFixer( model );

+ 90 - 21
packages/ckeditor5-table/src/tableui.js

@@ -9,6 +9,7 @@
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
 import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview';
 import Model from '@ckeditor/ckeditor5-ui/src/model';
 import Model from '@ckeditor/ckeditor5-ui/src/model';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
 
@@ -25,7 +26,7 @@ import tableMergeCellIcon from './../theme/icons/table-merge-cell.svg';
  * * The `'insertTable'` dropdown,
  * * The `'insertTable'` dropdown,
  * * The `'tableColumn'` dropdown,
  * * The `'tableColumn'` dropdown,
  * * The `'tableRow'` dropdown,
  * * The `'tableRow'` dropdown,
- * * The `'mergeTableCells'` dropdown.
+ * * The `'mergeTableCells'` split button.
  *
  *
  * The `'tableColumn'`, `'tableRow'` and `'mergeTableCells'` dropdowns work best with {@link module:table/tabletoolbar~TableToolbar}.
  * The `'tableColumn'`, `'tableRow'` and `'mergeTableCells'` dropdowns work best with {@link module:table/tabletoolbar~TableToolbar}.
  *
  *
@@ -113,6 +114,13 @@ export default class TableUI extends Plugin {
 						commandName: 'removeTableColumn',
 						commandName: 'removeTableColumn',
 						label: t( 'Delete column' )
 						label: t( 'Delete column' )
 					}
 					}
+				},
+				{
+					type: 'button',
+					model: {
+						commandName: 'selectTableColumn',
+						label: t( 'Select column' )
+					}
 				}
 				}
 			];
 			];
 
 
@@ -150,6 +158,13 @@ export default class TableUI extends Plugin {
 						commandName: 'removeTableRow',
 						commandName: 'removeTableRow',
 						label: t( 'Delete row' )
 						label: t( 'Delete row' )
 					}
 					}
+				},
+				{
+					type: 'button',
+					model: {
+						commandName: 'selectTableRow',
+						label: t( 'Select row' )
+					}
 				}
 				}
 			];
 			];
 
 
@@ -187,14 +202,6 @@ export default class TableUI extends Plugin {
 					}
 					}
 				},
 				},
 				{ type: 'separator' },
 				{ type: 'separator' },
-				{
-					type: 'button',
-					model: {
-						commandName: 'mergeTableCells',
-						label: t( 'Merge cells' )
-					}
-				},
-				{ type: 'separator' },
 				{
 				{
 					type: 'button',
 					type: 'button',
 					model: {
 					model: {
@@ -211,7 +218,7 @@ export default class TableUI extends Plugin {
 				}
 				}
 			];
 			];
 
 
-			return this._prepareDropdown( t( 'Merge cells' ), tableMergeCellIcon, options, locale );
+			return this._prepareMergeSplitButtonDropdown( t( 'Merge cells' ), tableMergeCellIcon, options, locale );
 		} );
 		} );
 	}
 	}
 
 
@@ -227,18 +234,8 @@ export default class TableUI extends Plugin {
 	 */
 	 */
 	_prepareDropdown( label, icon, options, locale ) {
 	_prepareDropdown( label, icon, options, locale ) {
 		const editor = this.editor;
 		const editor = this.editor;
-
 		const dropdownView = createDropdown( locale );
 		const dropdownView = createDropdown( locale );
-		const commands = [];
-
-		// Prepare dropdown list items for list dropdown.
-		const itemDefinitions = new Collection();
-
-		for ( const option of options ) {
-			addListOption( option, editor, commands, itemDefinitions );
-		}
-
-		addListToDropdown( dropdownView, itemDefinitions, editor.ui.componentFactory );
+		const commands = this._fillDropdownWithListOptions( dropdownView, options );
 
 
 		// Decorate dropdown's button.
 		// Decorate dropdown's button.
 		dropdownView.buttonView.set( {
 		dropdownView.buttonView.set( {
@@ -259,6 +256,78 @@ export default class TableUI extends Plugin {
 
 
 		return dropdownView;
 		return dropdownView;
 	}
 	}
+
+	/**
+	 * Creates a dropdown view with a {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} for
+	 * merge (and split)–related commands.
+	 *
+	 * @private
+	 * @param {String} label The dropdown button label.
+	 * @param {String} icon An icon for the dropdown button.
+	 * @param {Array.<module:ui/dropdown/utils~ListDropdownItemDefinition>} options The list of options for the dropdown.
+	 * @param {module:utils/locale~Locale} locale
+	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
+	 */
+	_prepareMergeSplitButtonDropdown( label, icon, options, locale ) {
+		const editor = this.editor;
+		const dropdownView = createDropdown( locale, SplitButtonView );
+		const mergeCommandName = 'mergeTableCells';
+
+		this._fillDropdownWithListOptions( dropdownView, options );
+
+		dropdownView.buttonView.set( {
+			label,
+			icon,
+			tooltip: true
+		} );
+
+		// The main part of the split button is bound to the "mergeTableCells" command only.
+		dropdownView.bind( 'isEnabled' ).to( editor.commands.get( mergeCommandName ) );
+
+		// The split button dropdown must be **always** enabled and ready to open no matter the state
+		// of the "mergeTableCells" command. You may not be able to merge multiple cells but you may want
+		// to split them. This is also about mobile devices where multi–cell selection will never work
+		// (that's why "Merge cell right", "Merge cell down", etc. are still there in the first place).
+		dropdownView.buttonView.arrowView.unbind( 'isEnabled' );
+		dropdownView.buttonView.arrowView.isEnabled = true;
+
+		// Merge selected table cells when the main part of the split button is clicked.
+		this.listenTo( dropdownView.buttonView, 'execute', () => {
+			editor.execute( mergeCommandName );
+			editor.editing.view.focus();
+		} );
+
+		// Execute commands for events coming from the list in the dropdown panel.
+		this.listenTo( dropdownView, 'execute', evt => {
+			editor.execute( evt.source.commandName );
+			editor.editing.view.focus();
+		} );
+
+		return dropdownView;
+	}
+
+	/**
+	 * Injects a {@link module:ui/list/listview~ListView} into the passed dropdown with buttons
+	 * which execute editor commands as configured in passed options.
+	 *
+	 * @private
+	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
+	 * @param {Array.<module:ui/dropdown/utils~ListDropdownItemDefinition>} options The list of options for the dropdown.
+	 * @returns {Array.<module:core/command~Command>} Commands the list options are interacting with.
+	 */
+	_fillDropdownWithListOptions( dropdownView, options ) {
+		const editor = this.editor;
+		const commands = [];
+		const itemDefinitions = new Collection();
+
+		for ( const option of options ) {
+			addListOption( option, editor, commands, itemDefinitions );
+		}
+
+		addListToDropdown( dropdownView, itemDefinitions, editor.ui.componentFactory );
+
+		return commands;
+	}
 }
 }
 
 
 // Adds an option to a list view.
 // Adds an option to a list view.

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

@@ -108,7 +108,7 @@ export default class TableUtils extends Plugin {
 	 *		    |   | f | g |                      |   |   |   |
 	 *		    |   | f | g |                      |   |   |   |
 	 *		  3 +---+---+---+                      +   +---+---+ 3
 	 *		  3 +---+---+---+                      +   +---+---+ 3
 	 *		                                       |   | d | e |
 	 *		                                       |   | d | e |
-	 *		                                       +---+---+---+ 4
+	 *		                                       +   +---+---+ 4
 	 *		                                       +   + f | g |
 	 *		                                       +   + f | g |
 	 *		                                       +---+---+---+ 5
 	 *		                                       +---+---+---+ 5
 	 *
 	 *
@@ -254,26 +254,26 @@ export default class TableUtils extends Plugin {
 	}
 	}
 
 
 	/**
 	/**
-	 * Removes a row from the given `table`.
+	 * Removes rows from the given `table`.
 	 *
 	 *
-	 * This method properly re-calculate table geometry including `rowspan` attribute of any table cell that is overlapping removed row
+	 * This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
 	 * and table headings values.
 	 * and table headings values.
 	 *
 	 *
 	 *		editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
 	 *		editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
 	 *
 	 *
-	 * Assuming the table on the left, the above code will transform it to the table on the right:
+	 * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
 	 *
 	 *
 	 *		row index
 	 *		row index
 	 *		    ┌───┬───┬───┐        `at` = 1        ┌───┬───┬───┐
 	 *		    ┌───┬───┬───┐        `at` = 1        ┌───┬───┬───┐
 	 *		  0 │ a │ b │ c │        `rows` = 2      │ a │ b │ c │ 0
 	 *		  0 │ a │ b │ c │        `rows` = 2      │ a │ b │ c │ 0
 	 *		    │   ├───┼───┤                        │   ├───┼───┤
 	 *		    │   ├───┼───┤                        │   ├───┼───┤
-	 *		  1 │   │ d │ e │  <-- remove from here  │   │ i │ j │ 1
+	 *		  1 │   │ d │ e │  <-- remove from here  │   │ h │ i │ 1
 	 *		    │   ├───┼───┤        will give:      ├───┼───┼───┤
 	 *		    │   ├───┼───┤        will give:      ├───┼───┼───┤
-	 *		  2 │   │ g │ h │                        │ k │ l │ m │ 2
+	 *		  2 │   │ f │ g │                        │ j │ k │ l │ 2
 	 *		    │   ├───┼───┤                        └───┴───┴───┘
 	 *		    │   ├───┼───┤                        └───┴───┴───┘
-	 *		  3 │   │ i │ j
+	 *		  3 │   │ h │ i
 	 *		    ├───┼───┼───┤
 	 *		    ├───┼───┼───┤
-	 *		  4 │ k │ l │ m │
+	 *		  4 │ j │ k │ l │
 	 *		    └───┴───┴───┘
 	 *		    └───┴───┴───┘
 	 *
 	 *
 	 * @param {module:engine/model/element~Element} table
 	 * @param {module:engine/model/element~Element} table

+ 39 - 21
packages/ckeditor5-table/src/ui/inserttableview.js

@@ -34,7 +34,7 @@ export default class InsertTableView extends View {
 		 * @readonly
 		 * @readonly
 		 * @member {module:ui/viewcollection~ViewCollection}
 		 * @member {module:ui/viewcollection~ViewCollection}
 		 */
 		 */
-		this.items = this.createCollection();
+		this.items = this._createGridCollection();
 
 
 		/**
 		/**
 		 * The currently selected number of rows of the new table.
 		 * The currently selected number of rows of the new table.
@@ -73,6 +73,9 @@ export default class InsertTableView extends View {
 					attributes: {
 					attributes: {
 						class: [ 'ck-insert-table-dropdown__grid' ]
 						class: [ 'ck-insert-table-dropdown__grid' ]
 					},
 					},
+					on: {
+						'mouseover@.ck-insert-table-dropdown-grid-box': bind.to( 'boxover' )
+					},
 					children: this.items
 					children: this.items
 				},
 				},
 				{
 				{
@@ -99,23 +102,15 @@ export default class InsertTableView extends View {
 			}
 			}
 		} );
 		} );
 
 
-		// Add grid boxes to table selection view.
-		for ( let index = 0; index < 100; index++ ) {
-			const boxView = new TableSizeGridBoxView();
-
-			// Listen to box view 'over' event which indicates that mouse is over this box.
-			boxView.on( 'over', () => {
-				// Translate box index to the row & column index.
-				const row = Math.floor( index / 10 );
-				const column = index % 10;
+		this.on( 'boxover', ( evt, domEvt ) => {
+			const { row, column } = domEvt.target.dataset;
 
 
-				// As row & column indexes are zero-based transform it to number of selected rows & columns.
-				this.set( 'rows', row + 1 );
-				this.set( 'columns', column + 1 );
+			// As row & column indexes are zero-based transform it to number of selected rows & columns.
+			this.set( {
+				rows: parseInt( row ),
+				columns: parseInt( column )
 			} );
 			} );
-
-			this.items.add( boxView );
-		}
+		} );
 
 
 		this.on( 'change:columns', () => {
 		this.on( 'change:columns', () => {
 			this._highlightGridBoxes();
 			this._highlightGridBoxes();
@@ -162,6 +157,30 @@ export default class InsertTableView extends View {
 			boxView.set( 'isOn', isOn );
 			boxView.set( 'isOn', isOn );
 		} );
 		} );
 	}
 	}
+
+	/**
+	 * @private
+	 * @returns {module:ui/viewcollection~ViewCollection} A view collection containing boxes to be placed in a table grid.
+	 */
+	_createGridCollection() {
+		const boxes = [];
+
+		// Add grid boxes to table selection view.
+		for ( let index = 0; index < 100; index++ ) {
+			const row = Math.floor( index / 10 );
+			const column = index % 10;
+
+			boxes.push( new TableSizeGridBoxView( this.locale, row + 1, column + 1 ) );
+		}
+
+		return this.createCollection( boxes );
+	}
+
+	/**
+	 * Fired when the mouse hover over one of the {@link #items child grid boxes}.
+	 *
+	 * @event boxover
+	 */
 }
 }
 
 
 /**
 /**
@@ -175,7 +194,7 @@ class TableSizeGridBoxView extends View {
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
-	constructor( locale ) {
+	constructor( locale, row, column ) {
 		super( locale );
 		super( locale );
 
 
 		const bind = this.bindTemplate;
 		const bind = this.bindTemplate;
@@ -194,10 +213,9 @@ class TableSizeGridBoxView extends View {
 				class: [
 				class: [
 					'ck-insert-table-dropdown-grid-box',
 					'ck-insert-table-dropdown-grid-box',
 					bind.if( 'isOn', 'ck-on' )
 					bind.if( 'isOn', 'ck-on' )
-				]
-			},
-			on: {
-				mouseover: bind.to( 'over' )
+				],
+				'data-row': row,
+				'data-column': column
 			}
 			}
 		} );
 		} );
 	}
 	}

+ 37 - 0
packages/ckeditor5-table/tests/_utils/utils.js

@@ -340,6 +340,43 @@ export function assertTableCellStyle( editor, tableCellStyle ) {
  * The above call will check if table has second column selected (assuming no spans).
  * The above call will check if table has second column selected (assuming no spans).
  *
  *
  * **Note**: This function operates on child indexes - not rows/columns.
  * **Note**: This function operates on child indexes - not rows/columns.
+ *
+ * Examples:
+ *
+ * 		+----+----+----+----+
+ * 		| 00 | 01 | 02 | 03 |
+ * 		+----+    +----+----+
+ * 		|[10]|    |[12]|[13]|
+ * 		+----+----+----+----+
+ * 		| 20 | 21 | 22 | 23 |
+ * 		+----+----+----+----+
+ * 		| 30 | 31      | 33 |
+ * 		+----+----+----+----+
+ *
+ * 		assertSelectedCells( model, [
+ *			[ 0, 0, 0, 0 ],
+ * 			[ 1,    1, 1 ],
+ * 			[ 0, 0, 0, 0 ],
+ *			[ 0, 0,    0 ]
+ *		] );
+ *
+ * 		+----+----+----+----+
+ * 		| 00 |[01]| 02 | 03 |
+ * 		+----+    +----+----+
+ * 		| 10 |    | 12 | 13 |
+ * 		+----+----+----+----+
+ * 		| 20 |[21]| 22 | 23 |
+ * 		+----+----+----+----+
+ * 		| 30 |[31]     | 33 |
+ * 		+----+----+----+----+
+ *
+ * 		assertSelectedCells( model, [
+ *			[ 0, 1, 0, 0 ],
+ * 			[ 0,    0, 0 ],
+ * 			[ 0, 1, 0, 0 ],
+ *			[ 0, 1,    0 ]
+ *		] );
+ *
  */
  */
 export function assertSelectedCells( model, tableMap ) {
 export function assertSelectedCells( model, tableMap ) {
 	const tableIndex = 0;
 	const tableIndex = 0;

+ 2 - 2
packages/ckeditor5-table/tests/commands/removerowcommand.js

@@ -464,7 +464,7 @@ describe( 'RemoveRowCommand', () => {
 			setData( model, modelTable( [
 			setData( model, modelTable( [
 				[ { rowspan: 3, contents: '[]00' }, { rowspan: 2, contents: '01' }, '02' ],
 				[ { rowspan: 3, contents: '[]00' }, { rowspan: 2, contents: '01' }, '02' ],
 				[ '12' ],
 				[ '12' ],
-				[ '22' ],
+				[ '21', '22' ],
 				[ '30', '31', '32' ]
 				[ '30', '31', '32' ]
 			] ) );
 			] ) );
 
 
@@ -472,7 +472,7 @@ describe( 'RemoveRowCommand', () => {
 
 
 			assertEqualMarkup( getData( model ), modelTable( [
 			assertEqualMarkup( getData( model ), modelTable( [
 				[ { rowspan: 2, contents: '[]00' }, '01', '12' ],
 				[ { rowspan: 2, contents: '[]00' }, '01', '12' ],
-				[ '22' ],
+				[ '21', '22' ],
 				[ '30', '31', '32' ]
 				[ '30', '31', '32' ]
 			] ) );
 			] ) );
 		} );
 		} );

+ 450 - 0
packages/ckeditor5-table/tests/commands/selectcolumncommand.js

@@ -0,0 +1,450 @@
+/**
+ * @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 VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import SelectColumnCommand from '../../src/commands/selectcolumncommand';
+import TableSelection from '../../src/tableselection';
+import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+
+describe( 'SelectColumnCommand', () => {
+	let editor, model, modelRoot, command, tableSelection;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create( { plugins: [ TableSelection ] } )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				modelRoot = model.document.getRoot();
+				command = new SelectColumnCommand( editor );
+				tableSelection = editor.plugins.get( TableSelection );
+
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'should be true if the selection is inside table cell', () => {
+			setData( model, modelTable( [
+				[ '00[]', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true if the selection contains multiple cells', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+			);
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be false if the selection is outside a table', () => {
+			setData( model, '<paragraph>11[]</paragraph>' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should select a column of a table cell with a collapsed selection', () => {
+			setData( model, modelTable( [
+				[ '00', '01', '02' ],
+				[ '10', '[]11', '12' ],
+				[ '20', '21', '22' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 0, 1, 0 ],
+				[ 0, 1, 0 ],
+				[ 0, 1, 0 ]
+			] );
+		} );
+
+		it( 'should select a column of table cell with a collapsed selection in first table cell', () => {
+			setData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 0 ],
+				[ 1, 0 ],
+				[ 1, 0 ]
+			] );
+		} );
+
+		it( 'should select a column of table cell with a collapsed selection in last cell in the first column', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '10', '11' ],
+				[ '20[]', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 0 ],
+				[ 1, 0 ],
+				[ 1, 0 ]
+			] );
+		} );
+
+		it( 'should select a column of table cell with collapsed selection in the first cell of the last column', () => {
+			setData( model, modelTable( [
+				[ '00', '01[]' ],
+				[ '10', '11' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 0, 1 ],
+				[ 0, 1 ]
+			] );
+		} );
+
+		describe( 'with col-spanned cells', () => {
+			beforeEach( () => {
+				// +----+----+----+----+
+				// | 00                |
+				// +----+----+----+----+
+				// | 10           | 13 |
+				// +----+----+----+----+
+				// | 20      | 22 | 23 |
+				// +----+----+----+----+
+				// | 30 | 31      | 33 |
+				// +----+----+----+----+
+				// | 40 | 41 | 42 | 43 |
+				// +----+----+----+----+
+				setData( model, modelTable( [
+					[ { colspan: 4, contents: '00' } ],
+					[ { colspan: 3, contents: '10' }, '13' ],
+					[ { colspan: 2, contents: '20' }, '22', '23' ],
+					[ '30', { colspan: 2, contents: '31' }, '33' ],
+					[ '40', '41', '42', '43' ]
+				] ) );
+			} );
+
+			it( 'should select only one column if only one cell is selected', () => {
+				// Selection in cell 10.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 1,       0 ],
+					[ 0,    0, 0 ],
+					[ 0, 0,    0 ],
+					[ 0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1          ],
+					[ 1,       0 ],
+					[ 1,    0, 0 ],
+					[ 1, 0,    0 ],
+					[ 1, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should not select col-spanned columns that start in other column', () => {
+				// Selection in cell 42.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 4, 2 ] ),
+					modelRoot.getNodeByPath( [ 0, 4, 2 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 0,       0 ],
+					[ 0,    0, 0 ],
+					[ 0, 0,    0 ],
+					[ 0, 0, 1, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 0,       0 ],
+					[ 0,    1, 0 ],
+					[ 0, 0,    0 ],
+					[ 0, 0, 1, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should not select col-spanned columns that start in other column but include those that start in selected column', () => {
+				// Selection in cell 41.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 4, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 4, 1 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 0,       0 ],
+					[ 0,    0, 0 ],
+					[ 0, 0,    0 ],
+					[ 0, 1, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 0,       0 ],
+					[ 0,    0, 0 ],
+					[ 0, 1,    0 ],
+					[ 0, 1, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should select properly for multiple not spanned cells selected', () => {
+				// Selection in cells 40 - 41.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 4, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 4, 1 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 0,       0 ],
+					[ 0,    0, 0 ],
+					[ 0, 0,    0 ],
+					[ 1, 1, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1          ],
+					[ 1,       0 ],
+					[ 1,    0, 0 ],
+					[ 1, 1,    0 ],
+					[ 1, 1, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should select properly for multiple cells selected including spanned one', () => {
+				// Selection in cells 31 - 33.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 3, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 3, 2 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 0,       0 ],
+					[ 0,    0, 0 ],
+					[ 0, 1,    1 ],
+					[ 0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0          ],
+					[ 0,       1 ],
+					[ 0,    1, 1 ],
+					[ 0, 1,    1 ],
+					[ 0, 1, 1, 1 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+		} );
+
+		describe( 'with multiple columns selected', () => {
+			beforeEach( () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02', '03' ],
+					[ '10', '11', '12', '13' ],
+					[ '20', '21', '22', '23' ]
+				] ) );
+			} );
+
+			it( 'should properly select middle columns', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 2 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 1, 1, 0 ],
+					[ 0, 1, 1, 0 ],
+					[ 0, 1, 1, 0 ]
+				] );
+			} );
+
+			it( 'should properly select middle columns in reversed order', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 1, 1, 0 ],
+					[ 0, 1, 1, 0 ],
+					[ 0, 1, 1, 0 ]
+				] );
+			} );
+
+			it( 'should properly select tailing columns', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 2 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 3 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 0, 1, 1 ],
+					[ 0, 0, 1, 1 ],
+					[ 0, 0, 1, 1 ]
+				] );
+			} );
+
+			it( 'should properly select beginning columns', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0, 0 ],
+					[ 1, 1, 0, 0 ],
+					[ 1, 1, 0, 0 ]
+				] );
+			} );
+
+			it( 'should properly select multiple columns from square selection', () => {
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 2 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 1, 1, 0 ],
+					[ 0, 1, 1, 0 ],
+					[ 0, 1, 1, 0 ]
+				] );
+			} );
+
+			it( 'should support selecting mixed heading and cell columns', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02', '03' ],
+					[ '10', '11', '12', '13' ],
+					[ '20', '21', '22', '23' ]
+				], { headingRows: 1 } ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 1, 1, 0, 0 ],
+					[ 1, 1, 0, 0 ],
+					[ 1, 1, 0, 0 ]
+				] );
+			} );
+		} );
+
+		describe( 'with entire column selected', () => {
+			it( 'should select a column if all its cells are selected', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02' ],
+					[ '10', '11', '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 1, 0 ],
+					[ 0, 1, 0 ],
+					[ 0, 1, 0 ]
+				] );
+			} );
+
+			it( 'should properly select column if reversed selection is made', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 1, 0 ],
+					[ 1, 0 ]
+				] );
+			} );
+		} );
+	} );
+} );

+ 469 - 0
packages/ckeditor5-table/tests/commands/selectrowcommand.js

@@ -0,0 +1,469 @@
+/**
+ * @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 VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import SelectRowCommand from '../../src/commands/selectrowcommand';
+import TableSelection from '../../src/tableselection';
+import { assertSelectedCells, defaultConversion, defaultSchema, modelTable } from '../_utils/utils';
+
+describe( 'SelectRowCommand', () => {
+	let editor, model, modelRoot, command, tableSelection;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create( { plugins: [ TableSelection ] } )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				modelRoot = model.document.getRoot();
+				command = new SelectRowCommand( editor );
+				tableSelection = editor.plugins.get( TableSelection );
+
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'should be true if the selection is inside table cell', () => {
+			setData( model, modelTable( [
+				[ '00[]', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true if the selection contains multiple cells', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			tableSelection._setCellSelection(
+				modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+				modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+			);
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be false if the selection is outside a table', () => {
+			setData( model, '<paragraph>11[]</paragraph>' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should select a row of a table cell with a collapsed selection', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '[]10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 0, 0 ],
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should select a row of table cell with a collapsed selection in first table cell', () => {
+			setData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should select a row of table cell with a collapsed selection in last cell in the first column', () => {
+			setData( model, modelTable( [
+				[ '00', '01[]' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should select a row of table cell with collapsed selection in the first cell of the last column', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '[]10', '11' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 0, 0 ],
+				[ 1, 1 ]
+			] );
+		} );
+
+		describe( 'with row-spanned cells', () => {
+			beforeEach( () => {
+				// +----+----+----+----+----+
+				// | 00 | 01 | 02 | 03 | 04 |
+				// +    +    +    +----+----+
+				// |    |    |    | 13 | 14 |
+				// +    +    +----+    +----+
+				// |    |    | 22 |    | 24 |
+				// +    +----+----+----+----+
+				// |    | 31 | 32 | 33 | 34 |
+				// +----+----+----+----+----+
+				setData( model, modelTable( [
+					[ { rowspan: 4, contents: '00' }, { rowspan: 3, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
+					[ { rowspan: 2, contents: '13' }, '14' ],
+					[ '22', '23', '24' ],
+					[ '30', '31', '32', '33', '34' ]
+				] ) );
+			} );
+
+			it( 'should select only one row if only one cell is selected', () => {
+				// Selection in cell 01.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 1, 0, 0, 0 ],
+					[          0, 0 ],
+					[       0,    0 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1, 1, 1, 1, 1 ],
+					[          0, 0 ],
+					[       0,    0 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should not select row-spanned rows that start in other row', () => {
+				// Selection in cell 24.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0 ],
+					[          0, 0 ],
+					[       0,    1 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0 ],
+					[          0, 0 ],
+					[       1,    1 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should not select row-spanned rows that start in other row but include those that start in selected row', () => {
+				// Selection in cell 14.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0 ],
+					[          0, 1 ],
+					[       0,    0 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0 ],
+					[          1, 1 ],
+					[       0,    0 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should select properly for multiple not spanned cells selected', () => {
+				// Selection in cells 04 - 14.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 4 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 1 ],
+					[          0, 1 ],
+					[       0,    0 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 1, 1, 1, 1, 1 ],
+					[          1, 1 ],
+					[       0,    0 ],
+					[    0, 0, 0, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+
+			it( 'should select properly for multiple cells selected including spanned one', () => {
+				// Selection in cells 13 - 33.
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 3, 2 ] )
+				);
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0 ],
+					[          1, 0 ],
+					[       0,    0 ],
+					[    0, 0, 1, 0 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+
+				command.execute();
+
+				/* eslint-disable no-multi-spaces */
+				assertSelectedCells( model, [
+					[ 0, 0, 0, 0, 0 ],
+					[          1, 1 ],
+					[       1,    1 ],
+					[    1, 1, 1, 1 ]
+				] );
+				/* eslint-enable no-multi-spaces */
+			} );
+		} );
+
+		describe( 'with multiple rows selected', () => {
+			it( 'should properly select middle rows', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ],
+					[ '30', '31' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 0 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 0 ],
+					[ 1, 1 ],
+					[ 1, 1 ],
+					[ 0, 0 ]
+				] );
+			} );
+
+			it( 'should properly select middle rows in reversed order', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ],
+					[ '30', '31' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 0 ],
+					[ 1, 1 ],
+					[ 1, 1 ],
+					[ 0, 0 ]
+				] );
+			} );
+
+			it( 'should properly select tailing rows', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ],
+					[ '30', '31' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 2, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 3, 0 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 0 ],
+					[ 0, 0 ],
+					[ 1, 1 ],
+					[ 1, 1 ]
+				] );
+			} );
+
+			it( 'should properly select beginning rows', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ],
+					[ '30', '31' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 1, 1 ],
+					[ 1, 1 ],
+					[ 0, 0 ],
+					[ 0, 0 ]
+				] );
+			} );
+
+			it( 'should properly select multiple rows from square selection', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02' ],
+					[ '10', '11', '12' ],
+					[ '20', '21', '22' ],
+					[ '30', '31', '32' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 2, 1 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 0, 0 ],
+					[ 1, 1, 1 ],
+					[ 1, 1, 1 ],
+					[ 0, 0, 0 ]
+				] );
+			} );
+
+			it( 'should support selecting mixed heading and cell rows', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ]
+				], { headingColumns: 1 } ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 1, 1 ],
+					[ 1, 1 ],
+					[ 0, 0 ]
+				] );
+			} );
+		} );
+
+		describe( 'with entire row selected', () => {
+			it( 'should select a row if all its cells are selected', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ],
+					[ '20', '21' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 1, 0 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 1 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 0 ],
+					[ 1, 1 ],
+					[ 0, 0 ]
+				] );
+			} );
+
+			it( 'should properly select row if reversed selection is made', () => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '10', '11' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 0, 0 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 1, 1 ],
+					[ 0, 0 ]
+				] );
+			} );
+		} );
+	} );
+} );

+ 10 - 0
packages/ckeditor5-table/tests/tableediting.js

@@ -16,6 +16,8 @@ import InsertTableCommand from '../src/commands/inserttablecommand';
 import InsertColumnCommand from '../src/commands/insertcolumncommand';
 import InsertColumnCommand from '../src/commands/insertcolumncommand';
 import RemoveRowCommand from '../src/commands/removerowcommand';
 import RemoveRowCommand from '../src/commands/removerowcommand';
 import RemoveColumnCommand from '../src/commands/removecolumncommand';
 import RemoveColumnCommand from '../src/commands/removecolumncommand';
+import SelectRowCommand from '../src/commands/selectrowcommand';
+import SelectColumnCommand from '../src/commands/selectcolumncommand';
 import SplitCellCommand from '../src/commands/splitcellcommand';
 import SplitCellCommand from '../src/commands/splitcellcommand';
 import MergeCellCommand from '../src/commands/mergecellcommand';
 import MergeCellCommand from '../src/commands/mergecellcommand';
 import SetHeaderRowCommand from '../src/commands/setheaderrowcommand';
 import SetHeaderRowCommand from '../src/commands/setheaderrowcommand';
@@ -111,6 +113,14 @@ describe( 'TableEditing', () => {
 		expect( editor.commands.get( 'removeTableColumn' ) ).to.be.instanceOf( RemoveColumnCommand );
 		expect( editor.commands.get( 'removeTableColumn' ) ).to.be.instanceOf( RemoveColumnCommand );
 	} );
 	} );
 
 
+	it( 'adds selectRow command', () => {
+		expect( editor.commands.get( 'selectTableRow' ) ).to.be.instanceOf( SelectRowCommand );
+	} );
+
+	it( 'adds selectColumn command', () => {
+		expect( editor.commands.get( 'selectTableColumn' ) ).to.be.instanceOf( SelectColumnCommand );
+	} );
+
 	it( 'adds splitCellVertically command', () => {
 	it( 'adds splitCellVertically command', () => {
 		expect( editor.commands.get( 'splitTableCellVertically' ) ).to.be.instanceOf( SplitCellCommand );
 		expect( editor.commands.get( 'splitTableCellVertically' ) ).to.be.instanceOf( SplitCellCommand );
 	} );
 	} );

+ 56 - 22
packages/ckeditor5-table/tests/tableui.js

@@ -15,6 +15,7 @@ import InsertTableView from '../src/ui/inserttableview';
 import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview';
 import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview';
 import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
 import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
 import ListSeparatorView from '@ckeditor/ckeditor5-ui/src/list/listseparatorview';
 import ListSeparatorView from '@ckeditor/ckeditor5-ui/src/list/listseparatorview';
+import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview';
 
 
 describe( 'TableUI', () => {
 describe( 'TableUI', () => {
 	let editor, element;
 	let editor, element;
@@ -137,7 +138,9 @@ describe( 'TableUI', () => {
 
 
 			const labels = listView.items.map( item => item instanceof ListSeparatorView ? '|' : item.children.first.label );
 			const labels = listView.items.map( item => item instanceof ListSeparatorView ? '|' : item.children.first.label );
 
 
-			expect( labels ).to.deep.equal( [ 'Header row', '|', 'Insert row below', 'Insert row above', 'Delete row' ] );
+			expect( labels ).to.deep.equal(
+				[ 'Header row', '|', 'Insert row below', 'Insert row above', 'Delete row', 'Select row' ]
+			);
 		} );
 		} );
 
 
 		it( 'should bind items in panel to proper commands', () => {
 		it( 'should bind items in panel to proper commands', () => {
@@ -147,16 +150,19 @@ describe( 'TableUI', () => {
 			const insertRowBelowCommand = editor.commands.get( 'insertTableRowBelow' );
 			const insertRowBelowCommand = editor.commands.get( 'insertTableRowBelow' );
 			const insertRowAboveCommand = editor.commands.get( 'insertTableRowAbove' );
 			const insertRowAboveCommand = editor.commands.get( 'insertTableRowAbove' );
 			const removeRowCommand = editor.commands.get( 'removeTableRow' );
 			const removeRowCommand = editor.commands.get( 'removeTableRow' );
+			const selectRowCommand = editor.commands.get( 'selectTableRow' );
 
 
 			setRowHeaderCommand.isEnabled = true;
 			setRowHeaderCommand.isEnabled = true;
 			insertRowBelowCommand.isEnabled = true;
 			insertRowBelowCommand.isEnabled = true;
 			insertRowAboveCommand.isEnabled = true;
 			insertRowAboveCommand.isEnabled = true;
 			removeRowCommand.isEnabled = true;
 			removeRowCommand.isEnabled = true;
+			selectRowCommand.isEnabled = true;
 
 
 			expect( items.first.children.first.isEnabled ).to.be.true;
 			expect( items.first.children.first.isEnabled ).to.be.true;
 			expect( items.get( 2 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 2 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 3 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 3 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.true;
+			expect( items.get( 5 ).children.first.isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
 
 			setRowHeaderCommand.isEnabled = false;
 			setRowHeaderCommand.isEnabled = false;
@@ -176,6 +182,11 @@ describe( 'TableUI', () => {
 			removeRowCommand.isEnabled = false;
 			removeRowCommand.isEnabled = false;
 
 
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.false;
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			selectRowCommand.isEnabled = false;
+
+			expect( items.get( 5 ).children.first.isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 		} );
 		} );
 
 
@@ -238,7 +249,9 @@ describe( 'TableUI', () => {
 
 
 			const labels = listView.items.map( item => item instanceof ListSeparatorView ? '|' : item.children.first.label );
 			const labels = listView.items.map( item => item instanceof ListSeparatorView ? '|' : item.children.first.label );
 
 
-			expect( labels ).to.deep.equal( [ 'Header column', '|', 'Insert column left', 'Insert column right', 'Delete column' ] );
+			expect( labels ).to.deep.equal(
+				[ 'Header column', '|', 'Insert column left', 'Insert column right', 'Delete column', 'Select column' ]
+			);
 		} );
 		} );
 
 
 		it( 'should bind items in panel to proper commands (LTR content)', () => {
 		it( 'should bind items in panel to proper commands (LTR content)', () => {
@@ -248,16 +261,19 @@ describe( 'TableUI', () => {
 			const insertColumnLeftCommand = editor.commands.get( 'insertTableColumnLeft' );
 			const insertColumnLeftCommand = editor.commands.get( 'insertTableColumnLeft' );
 			const insertColumnRightCommand = editor.commands.get( 'insertTableColumnRight' );
 			const insertColumnRightCommand = editor.commands.get( 'insertTableColumnRight' );
 			const removeColumnCommand = editor.commands.get( 'removeTableColumn' );
 			const removeColumnCommand = editor.commands.get( 'removeTableColumn' );
+			const selectColumnCommand = editor.commands.get( 'selectTableColumn' );
 
 
 			setColumnHeaderCommand.isEnabled = true;
 			setColumnHeaderCommand.isEnabled = true;
 			insertColumnLeftCommand.isEnabled = true;
 			insertColumnLeftCommand.isEnabled = true;
 			insertColumnRightCommand.isEnabled = true;
 			insertColumnRightCommand.isEnabled = true;
 			removeColumnCommand.isEnabled = true;
 			removeColumnCommand.isEnabled = true;
+			selectColumnCommand.isEnabled = true;
 
 
 			expect( items.first.children.first.isEnabled ).to.be.true;
 			expect( items.first.children.first.isEnabled ).to.be.true;
 			expect( items.get( 2 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 2 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 3 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 3 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.true;
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.true;
+			expect( items.get( 5 ).children.first.isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
 
 			setColumnHeaderCommand.isEnabled = false;
 			setColumnHeaderCommand.isEnabled = false;
@@ -275,6 +291,10 @@ describe( 'TableUI', () => {
 
 
 			removeColumnCommand.isEnabled = false;
 			removeColumnCommand.isEnabled = false;
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.false;
 			expect( items.get( 4 ).children.first.isEnabled ).to.be.false;
+
+			selectColumnCommand.isEnabled = false;
+			expect( items.get( 5 ).children.first.isEnabled ).to.be.false;
+
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 		} );
 		} );
 
 
@@ -343,11 +363,12 @@ describe( 'TableUI', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'mergeTableCell dropdown', () => {
-		let dropdown;
+	describe( 'mergeTableCell split button', () => {
+		let dropdown, command;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
 			dropdown = editor.ui.componentFactory.create( 'mergeTableCells' );
 			dropdown = editor.ui.componentFactory.create( 'mergeTableCells' );
+			command = editor.commands.get( 'mergeTableCells' );
 		} );
 		} );
 
 
 		it( 'have button with proper properties set', () => {
 		it( 'have button with proper properties set', () => {
@@ -361,6 +382,35 @@ describe( 'TableUI', () => {
 			expect( button.icon ).to.match( /<svg / );
 			expect( button.icon ).to.match( /<svg / );
 		} );
 		} );
 
 
+		it( 'should have a split button', () => {
+			expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView );
+		} );
+
+		it( 'should bind #isEnabled to the "mergeTableCells" command', () => {
+			command.isEnabled = false;
+			expect( dropdown.isEnabled ).to.be.false;
+
+			command.isEnabled = true;
+			expect( dropdown.isEnabled ).to.be.true;
+		} );
+
+		it( 'should execute the "mergeTableCells" command when the main part of the split button is clicked', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			dropdown.buttonView.fire( 'execute' );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, 'mergeTableCells' );
+		} );
+
+		it( 'should have the dropdown part of the split button always enabled no matter the "mergeTableCells" command state', () => {
+			command.isEnabled = true;
+			expect( dropdown.buttonView.arrowView.isEnabled ).to.be.true;
+
+			command.isEnabled = false;
+			expect( dropdown.buttonView.arrowView.isEnabled ).to.be.true;
+		} );
+
 		it( 'should have proper items in panel', () => {
 		it( 'should have proper items in panel', () => {
 			const listView = dropdown.listView;
 			const listView = dropdown.listView;
 
 
@@ -372,8 +422,6 @@ describe( 'TableUI', () => {
 				'Merge cell down',
 				'Merge cell down',
 				'Merge cell left',
 				'Merge cell left',
 				'|',
 				'|',
-				'Merge cells',
-				'|',
 				'Split cell vertically',
 				'Split cell vertically',
 				'Split cell horizontally'
 				'Split cell horizontally'
 			] );
 			] );
@@ -386,7 +434,6 @@ describe( 'TableUI', () => {
 			const mergeCellRightCommand = editor.commands.get( 'mergeTableCellRight' );
 			const mergeCellRightCommand = editor.commands.get( 'mergeTableCellRight' );
 			const mergeCellDownCommand = editor.commands.get( 'mergeTableCellDown' );
 			const mergeCellDownCommand = editor.commands.get( 'mergeTableCellDown' );
 			const mergeCellLeftCommand = editor.commands.get( 'mergeTableCellLeft' );
 			const mergeCellLeftCommand = editor.commands.get( 'mergeTableCellLeft' );
-			const mergeCellsCommand = editor.commands.get( 'mergeTableCells' );
 			const splitCellVerticallyCommand = editor.commands.get( 'splitTableCellVertically' );
 			const splitCellVerticallyCommand = editor.commands.get( 'splitTableCellVertically' );
 			const splitCellHorizontallyCommand = editor.commands.get( 'splitTableCellHorizontally' );
 			const splitCellHorizontallyCommand = editor.commands.get( 'splitTableCellHorizontally' );
 
 
@@ -394,7 +441,6 @@ describe( 'TableUI', () => {
 			mergeCellRightCommand.isEnabled = true;
 			mergeCellRightCommand.isEnabled = true;
 			mergeCellDownCommand.isEnabled = true;
 			mergeCellDownCommand.isEnabled = true;
 			mergeCellLeftCommand.isEnabled = true;
 			mergeCellLeftCommand.isEnabled = true;
-			mergeCellsCommand.isEnabled = true;
 			splitCellVerticallyCommand.isEnabled = true;
 			splitCellVerticallyCommand.isEnabled = true;
 			splitCellHorizontallyCommand.isEnabled = true;
 			splitCellHorizontallyCommand.isEnabled = true;
 
 
@@ -403,28 +449,21 @@ describe( 'TableUI', () => {
 			const mergeCellDownButton = items.get( 2 );
 			const mergeCellDownButton = items.get( 2 );
 			const mergeCellLeftButton = items.get( 3 );
 			const mergeCellLeftButton = items.get( 3 );
 			// separator
 			// separator
-			const mergeCellsButton = items.get( 5 );
-			// separator
-			const splitVerticallyButton = items.get( 7 );
-			const splitHorizontallyButton = items.get( 8 );
+			const splitVerticallyButton = items.get( 5 );
+			const splitHorizontallyButton = items.get( 6 );
 
 
 			expect( mergeCellUpButton.children.first.isEnabled ).to.be.true;
 			expect( mergeCellUpButton.children.first.isEnabled ).to.be.true;
 			expect( mergeCellRightButton.children.first.isEnabled ).to.be.true;
 			expect( mergeCellRightButton.children.first.isEnabled ).to.be.true;
 			expect( mergeCellDownButton.children.first.isEnabled ).to.be.true;
 			expect( mergeCellDownButton.children.first.isEnabled ).to.be.true;
 			expect( mergeCellLeftButton.children.first.isEnabled ).to.be.true;
 			expect( mergeCellLeftButton.children.first.isEnabled ).to.be.true;
-			expect( mergeCellsButton.children.first.isEnabled ).to.be.true;
 			expect( splitVerticallyButton.children.first.isEnabled ).to.be.true;
 			expect( splitVerticallyButton.children.first.isEnabled ).to.be.true;
 			expect( splitHorizontallyButton.children.first.isEnabled ).to.be.true;
 			expect( splitHorizontallyButton.children.first.isEnabled ).to.be.true;
-			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
 
 			mergeCellUpCommand.isEnabled = false;
 			mergeCellUpCommand.isEnabled = false;
 			expect( mergeCellUpButton.children.first.isEnabled ).to.be.false;
 			expect( mergeCellUpButton.children.first.isEnabled ).to.be.false;
-			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
 
 			mergeCellRightCommand.isEnabled = false;
 			mergeCellRightCommand.isEnabled = false;
-
 			expect( mergeCellRightButton.children.first.isEnabled ).to.be.false;
 			expect( mergeCellRightButton.children.first.isEnabled ).to.be.false;
-			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
 
 			mergeCellDownCommand.isEnabled = false;
 			mergeCellDownCommand.isEnabled = false;
 			expect( mergeCellDownButton.children.first.isEnabled ).to.be.false;
 			expect( mergeCellDownButton.children.first.isEnabled ).to.be.false;
@@ -432,16 +471,11 @@ describe( 'TableUI', () => {
 			mergeCellLeftCommand.isEnabled = false;
 			mergeCellLeftCommand.isEnabled = false;
 			expect( mergeCellLeftButton.children.first.isEnabled ).to.be.false;
 			expect( mergeCellLeftButton.children.first.isEnabled ).to.be.false;
 
 
-			mergeCellsCommand.isEnabled = false;
-			expect( mergeCellsButton.children.first.isEnabled ).to.be.false;
-
 			splitCellVerticallyCommand.isEnabled = false;
 			splitCellVerticallyCommand.isEnabled = false;
 			expect( splitVerticallyButton.children.first.isEnabled ).to.be.false;
 			expect( splitVerticallyButton.children.first.isEnabled ).to.be.false;
 
 
 			splitCellHorizontallyCommand.isEnabled = false;
 			splitCellHorizontallyCommand.isEnabled = false;
 			expect( splitHorizontallyButton.children.first.isEnabled ).to.be.false;
 			expect( splitHorizontallyButton.children.first.isEnabled ).to.be.false;
-
-			expect( dropdown.buttonView.isEnabled ).to.be.false;
 		} );
 		} );
 
 
 		it( 'should bind items in panel to proper commands (RTL content)', () => {
 		it( 'should bind items in panel to proper commands (RTL content)', () => {

+ 16 - 16
packages/ckeditor5-table/tests/tableutils.js

@@ -748,7 +748,7 @@ describe( 'TableUtils', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'removeRow()', () => {
+	describe( 'removeRows()', () => {
 		describe( 'single row', () => {
 		describe( 'single row', () => {
 			it( 'should remove a given row from a table start', () => {
 			it( 'should remove a given row from a table start', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [
@@ -757,7 +757,7 @@ describe( 'TableUtils', () => {
 					[ '20', '21' ]
 					[ '20', '21' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 0 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '10', '11' ],
 					[ '10', '11' ],
@@ -771,7 +771,7 @@ describe( 'TableUtils', () => {
 					[ '10', '11' ]
 					[ '10', '11' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 1 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '00', '01' ]
 					[ '00', '01' ]
@@ -785,7 +785,7 @@ describe( 'TableUtils', () => {
 					[ '20', '21' ]
 					[ '20', '21' ]
 				], { headingRows: 2 } ) );
 				], { headingRows: 2 } ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 1 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 1 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '00', '01' ],
 					[ '00', '01' ],
@@ -801,7 +801,7 @@ describe( 'TableUtils', () => {
 					[ '30', '31', '32', '33', '34' ]
 					[ '30', '31', '32', '33', '34' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 2 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
 					[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, { rowspan: 2, contents: '02' }, '03', '04' ],
@@ -814,15 +814,15 @@ describe( 'TableUtils', () => {
 				setData( model, modelTable( [
 				setData( model, modelTable( [
 					[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02' ],
 					[ { rowspan: 3, contents: '00' }, { rowspan: 2, contents: '01' }, '02' ],
 					[ '12' ],
 					[ '12' ],
-					[ '22' ],
+					[ '21', '22' ],
 					[ '30', '31', '32' ]
 					[ '30', '31', '32' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 0 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ { rowspan: 2, contents: '00' }, '01', '12' ],
 					[ { rowspan: 2, contents: '00' }, '01', '12' ],
-					[ '22' ],
+					[ '21', '22' ],
 					[ '30', '31', '32' ]
 					[ '30', '31', '32' ]
 				] ) );
 				] ) );
 			} );
 			} );
@@ -837,7 +837,7 @@ describe( 'TableUtils', () => {
 					[ '30', '31' ]
 					[ '30', '31' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 1, rows: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 2 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '00', '01' ],
 					[ '00', '01' ],
@@ -853,7 +853,7 @@ describe( 'TableUtils', () => {
 					[ '30', '31' ]
 					[ '30', '31' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 2, rows: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 2, rows: 2 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '00', '01' ],
 					[ '00', '01' ],
@@ -869,7 +869,7 @@ describe( 'TableUtils', () => {
 					[ '30', '31' ]
 					[ '30', '31' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 0, rows: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '20', '21' ],
 					[ '20', '21' ],
@@ -885,7 +885,7 @@ describe( 'TableUtils', () => {
 					[ '30', '31' ]
 					[ '30', '31' ]
 				], { headingRows: 3 } ) );
 				], { headingRows: 3 } ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 0, rows: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '20', '21' ],
 					[ '20', '21' ],
@@ -902,7 +902,7 @@ describe( 'TableUtils', () => {
 					[ '40', '41' ]
 					[ '40', '41' ]
 				], { headingRows: 3 } ) );
 				], { headingRows: 3 } ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 1, rows: 3 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 1, rows: 3 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '00', '01' ],
 					[ '00', '01' ],
@@ -917,7 +917,7 @@ describe( 'TableUtils', () => {
 					[ '20', '21' ]
 					[ '20', '21' ]
 				], { headingRows: 1 } ) );
 				], { headingRows: 1 } ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 0, rows: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '20', '21' ]
 					[ '20', '21' ]
@@ -931,7 +931,7 @@ describe( 'TableUtils', () => {
 					[ '20' ]
 					[ '20' ]
 				] ) );
 				] ) );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 0, rows: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } );
 
 
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 				assertEqualMarkup( getData( model, { withoutSelection: true } ), modelTable( [
 					[ '20', '01' ]
 					[ '20', '01' ]
@@ -954,7 +954,7 @@ describe( 'TableUtils', () => {
 					createdBatches.add( operation.batch );
 					createdBatches.add( operation.batch );
 				} );
 				} );
 
 
-				tableUtils.removeRows( root.getNodeByPath( [ 0 ] ), { at: 0, rows: 2 } );
+				tableUtils.removeRows( root.getChild( 0 ), { at: 0, rows: 2 } );
 
 
 				expect( createdBatches.size ).to.equal( 1 );
 				expect( createdBatches.size ).to.equal( 1 );
 			} );
 			} );

+ 3 - 16
packages/ckeditor5-table/tests/ui/inserttableview.js

@@ -60,12 +60,12 @@ describe( 'InsertTableView', () => {
 		} );
 		} );
 
 
 		describe( 'view#items bindings', () => {
 		describe( 'view#items bindings', () => {
-			it( 'updates view#height & view#width on "over" event', () => {
+			it( 'updates view#height & view#width on DOM "mouseover" event', () => {
 				const boxView = view.items.get( 0 );
 				const boxView = view.items.get( 0 );
 
 
 				expect( boxView.isOn ).to.be.false;
 				expect( boxView.isOn ).to.be.false;
 
 
-				boxView.fire( 'over' );
+				boxView.element.dispatchEvent( new Event( 'mouseover', { bubbles: true } ) );
 
 
 				expect( boxView.isOn ).to.be.true;
 				expect( boxView.isOn ).to.be.true;
 
 
@@ -74,7 +74,7 @@ describe( 'InsertTableView', () => {
 
 
 				const boxViewB = view.items.get( 22 );
 				const boxViewB = view.items.get( 22 );
 
 
-				boxViewB.fire( 'over' );
+				boxViewB.element.dispatchEvent( new Event( 'mouseover', { bubbles: true } ) );
 
 
 				expect( view.rows ).to.equal( 3 );
 				expect( view.rows ).to.equal( 3 );
 				expect( view.columns ).to.equal( 3 );
 				expect( view.columns ).to.equal( 3 );
@@ -108,19 +108,6 @@ describe( 'InsertTableView', () => {
 
 
 					sinon.assert.calledOnce( spy );
 					sinon.assert.calledOnce( spy );
 				} );
 				} );
-
-				describe( 'view#items mousemove event', () => {
-					it( 'fires "over" event', () => {
-						const boxView = view.items.get( 0 );
-						const spy = sinon.spy();
-
-						boxView.on( 'over', spy );
-
-						dispatchEvent( boxView.element, 'mouseover' );
-
-						sinon.assert.calledOnce( spy );
-					} );
-				} );
 			} );
 			} );
 		} );
 		} );
 	} );
 	} );