8
0
Pārlūkot izejas kodu

Added SelectRowCommand and SelectColumnCommand.

Kuba Niegowski 5 gadi atpakaļ
vecāks
revīzija
5c2686dfb2

+ 8 - 0
packages/ckeditor5-table/docs/features/table.md

@@ -394,6 +394,14 @@ The {@link module:table/tabletoolbar~TableToolbar} plugin introduces two balloon
 			<td><code>'removeTableRow'</code></td>
 			<td>{@link module:table/commands/removerowcommand~RemoveRowCommand}</td>
 		</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>
 			<td><code>'setTableColumnHeader'</code></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 right": "Label for the insert table column to the right of the current one button.",
 	"Delete column": "Label for the delete table column button.",
+	"Select entire column": "Label for the select table entire column button.",
 	"Column": "Label for the table column dropdown button.",
 	"Header row": "Label for the set/unset table header row button.",
 	"Insert row below": "Label for the insert row below button.",
 	"Insert row above": "Label for the insert row above button.",
 	"Delete row": "Label for the delete table row button.",
+	"Select entire row": "Label for the select table entire row button.",
 	"Row": "Label for the table row dropdown button.",
 	"Merge cell up": "Label for the merge table cell up 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 column containing the selected cell, 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 cellsToSelect = [];
+
+		for ( const cellInfo of new TableWalker( findAncestor( 'table', firstCell ) ) ) {
+			if ( cellInfo.column >= startColumn && cellInfo.column <= endColumn ) {
+				cellsToSelect.push( cellInfo.cell );
+			}
+		}
+
+		model.change( writer => {
+			writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
+		} );
+	}
+}

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

@@ -0,0 +1,56 @@
+/**
+ * @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 TableWalker from '../tablewalker';
+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 row containing the selected cell, 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 cellsToSelect = [];
+
+		for ( const cellInfo of new TableWalker( table, { startRow: rowIndexes.first, endRow: rowIndexes.last } ) ) {
+			cellsToSelect.push( cellInfo.cell );
+		}
+
+		model.change( writer => {
+			writer.setSelection( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );
+		} );
+	}
+}

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

@@ -29,6 +29,8 @@ import RemoveColumnCommand from './commands/removecolumncommand';
 import SetHeaderRowCommand from './commands/setheaderrowcommand';
 import SetHeaderColumnCommand from './commands/setheadercolumncommand';
 import MergeCellsCommand from './commands/mergecellscommand';
+import SelectRowCommand from './commands/selectrowcommand';
+import SelectColumnCommand from './commands/selectcolumncommand';
 import { getTableCellsContainingSelection } from './utils';
 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( 'setTableRowHeader', new SetHeaderRowCommand( editor ) );
 
+		editor.commands.add( 'selectTableRow', new SelectRowCommand( editor ) );
+		editor.commands.add( 'selectTableColumn', new SelectColumnCommand( editor ) );
+
 		injectTableLayoutPostFixer( model );
 		injectTableCellRefreshPostFixer( model );
 		injectTableCellParagraphPostFixer( model );

+ 14 - 0
packages/ckeditor5-table/src/tableui.js

@@ -113,6 +113,13 @@ export default class TableUI extends Plugin {
 						commandName: 'removeTableColumn',
 						label: t( 'Delete column' )
 					}
+				},
+				{
+					type: 'button',
+					model: {
+						commandName: 'selectTableColumn',
+						label: t( 'Select entire column' )
+					}
 				}
 			];
 
@@ -150,6 +157,13 @@ export default class TableUI extends Plugin {
 						commandName: 'removeTableRow',
 						label: t( 'Delete row' )
 					}
+				},
+				{
+					type: 'button',
+					model: {
+						commandName: 'selectTableRow',
+						label: t( 'Select entire row' )
+					}
 				}
 			];
 

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

@@ -108,7 +108,7 @@ export default class TableUtils extends Plugin {
 	 *		    |   | f | g |                      |   |   |   |
 	 *		  3 +---+---+---+                      +   +---+---+ 3
 	 *		                                       |   | d | e |
-	 *		                                       +---+---+---+ 4
+	 *		                                       +   +---+---+ 4
 	 *		                                       +   + f | g |
 	 *		                                       +---+---+---+ 5
 	 *

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

@@ -338,8 +338,6 @@ export function assertTableCellStyle( editor, tableCellStyle ) {
  *		] );
  *
  * The above call will check if table has second column selected (assuming no spans).
- *
- * **Note**: This function operates on child indexes - not rows/columns.
  */
 export function assertSelectedCells( model, tableMap ) {
 	const tableIndex = 0;

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

@@ -0,0 +1,332 @@
+/**
+ * @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 selection is inside table cell', () => {
+			setData( model, modelTable( [
+				[ '00[]', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true if 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 selection is outside a table', () => {
+			setData( model, '<paragraph>11[]</paragraph>' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should select a given column', () => {
+			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 given column from a table start', () => {
+			setData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 0 ],
+				[ 1, 0 ],
+				[ 1, 0 ]
+			] );
+		} );
+
+		it( 'should select a given column from a table start when selection is at the end', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '10', '11' ],
+				[ '20[]', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 0 ],
+				[ 1, 0 ],
+				[ 1, 0 ]
+			] );
+		} );
+
+		it( 'should select last column', () => {
+			setData( model, modelTable( [
+				[ '00', '01[]' ],
+				[ '10', '11' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 0, 1 ],
+				[ 0, 1 ]
+			] );
+		} );
+
+		it( 'should select col-spanned cells', () => {
+			// +----+----+----+----+
+			// | 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' ]
+			] ) );
+
+			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 */
+		} );
+
+		describe( 'with multiple columns selected', () => {
+			it( 'should properly select middle columns', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02', '03' ],
+					[ '10', '11', '12', '13' ],
+					[ '20', '21', '22', '23' ]
+				] ) );
+
+				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', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02', '03' ],
+					[ '10', '11', '12', '13' ],
+					[ '20', '21', '22', '23' ]
+				] ) );
+
+				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', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02', '03' ],
+					[ '10', '11', '12', '13' ],
+					[ '20', '21', '22', '23' ]
+				] ) );
+
+				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', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02', '03' ],
+					[ '10', '11', '12', '13' ],
+					[ '20', '21', '22', '23' ]
+				] ) );
+
+				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', () => {
+				setData( model, modelTable( [
+					[ '00', '01', '02' ],
+					[ '10', '11', '12' ],
+					[ '20', '21', '22' ]
+				] ) );
+
+				tableSelection._setCellSelection(
+					modelRoot.getNodeByPath( [ 0, 0, 1 ] ),
+					modelRoot.getNodeByPath( [ 0, 1, 2 ] )
+				);
+
+				command.execute();
+
+				assertSelectedCells( model, [
+					[ 0, 1, 1 ],
+					[ 0, 1, 1 ],
+					[ 0, 1, 1 ]
+				] );
+			} );
+
+			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 ]
+				] );
+			} );
+		} );
+	} );
+} );

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

@@ -0,0 +1,338 @@
+/**
+ * @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 selection is inside table cell', () => {
+			setData( model, modelTable( [
+				[ '00[]', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true if 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 selection is outside a table', () => {
+			setData( model, '<paragraph>11[]</paragraph>' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should select a given row', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '[]10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 0, 0 ],
+				[ 1, 1 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should select a given row from a table start', () => {
+			setData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should select a given row from a table start when selection is at the end', () => {
+			setData( model, modelTable( [
+				[ '00', '01[]' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 1, 1 ],
+				[ 0, 0 ],
+				[ 0, 0 ]
+			] );
+		} );
+
+		it( 'should select last row', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '[]10', '11' ]
+			] ) );
+
+			command.execute();
+
+			assertSelectedCells( model, [
+				[ 0, 0 ],
+				[ 1, 1 ]
+			] );
+		} );
+
+		it( 'should select row-spanned cells', () => {
+			// +----+----+----+----+----+
+			// | 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' ]
+			] ) );
+
+			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 */
+		} );
+
+		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 RemoveRowCommand from '../src/commands/removerowcommand';
 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 MergeCellCommand from '../src/commands/mergecellcommand';
 import SetHeaderRowCommand from '../src/commands/setheaderrowcommand';
@@ -111,6 +113,14 @@ describe( 'TableEditing', () => {
 		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', () => {
 		expect( editor.commands.get( 'splitTableCellVertically' ) ).to.be.instanceOf( SplitCellCommand );
 	} );

+ 21 - 2
packages/ckeditor5-table/tests/tableui.js

@@ -137,7 +137,9 @@ describe( 'TableUI', () => {
 
 			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 entire row' ]
+			);
 		} );
 
 		it( 'should bind items in panel to proper commands', () => {
@@ -147,16 +149,19 @@ describe( 'TableUI', () => {
 			const insertRowBelowCommand = editor.commands.get( 'insertTableRowBelow' );
 			const insertRowAboveCommand = editor.commands.get( 'insertTableRowAbove' );
 			const removeRowCommand = editor.commands.get( 'removeTableRow' );
+			const selectRowCommand = editor.commands.get( 'selectTableRow' );
 
 			setRowHeaderCommand.isEnabled = true;
 			insertRowBelowCommand.isEnabled = true;
 			insertRowAboveCommand.isEnabled = true;
 			removeRowCommand.isEnabled = true;
+			selectRowCommand.isEnabled = true;
 
 			expect( items.first.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( 4 ).children.first.isEnabled ).to.be.true;
+			expect( items.get( 5 ).children.first.isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
 			setRowHeaderCommand.isEnabled = false;
@@ -176,6 +181,11 @@ describe( 'TableUI', () => {
 			removeRowCommand.isEnabled = 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;
 		} );
 
@@ -238,7 +248,9 @@ describe( 'TableUI', () => {
 
 			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 entire column' ]
+			);
 		} );
 
 		it( 'should bind items in panel to proper commands (LTR content)', () => {
@@ -248,16 +260,19 @@ describe( 'TableUI', () => {
 			const insertColumnLeftCommand = editor.commands.get( 'insertTableColumnLeft' );
 			const insertColumnRightCommand = editor.commands.get( 'insertTableColumnRight' );
 			const removeColumnCommand = editor.commands.get( 'removeTableColumn' );
+			const selectColumnCommand = editor.commands.get( 'selectTableColumn' );
 
 			setColumnHeaderCommand.isEnabled = true;
 			insertColumnLeftCommand.isEnabled = true;
 			insertColumnRightCommand.isEnabled = true;
 			removeColumnCommand.isEnabled = true;
+			selectColumnCommand.isEnabled = true;
 
 			expect( items.first.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( 4 ).children.first.isEnabled ).to.be.true;
+			expect( items.get( 5 ).children.first.isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
 			setColumnHeaderCommand.isEnabled = false;
@@ -275,6 +290,10 @@ describe( 'TableUI', () => {
 
 			removeColumnCommand.isEnabled = 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;
 		} );