Procházet zdrojové kódy

Other: Refactored SetTableHeadersCommand to two SetHeaderColumnCommand and SetHeaderRowCommand.

Maciej Gołaszewski před 7 roky
rodič
revize
4766e6e4ba

+ 78 - 0
packages/ckeditor5-table/src/commands/setheadercolumncommand.js

@@ -0,0 +1,78 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/commands/setheadercolumncommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+import { getParentTable, updateNumericAttribute } from './utils';
+
+/**
+ * The header coloumn command.
+ *
+ * @extends module:core/command~Command
+ */
+export default class SetHeaderColumnCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const doc = model.document;
+		const selection = doc.selection;
+
+		const position = selection.getFirstPosition();
+		const tableParent = getParentTable( position );
+
+		this.isEnabled = !!tableParent;
+
+		this.value = this.isEnabled && this._isInHeading( position.parent, tableParent );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	execute() {
+		const model = this.editor.model;
+		const doc = model.document;
+		const selection = doc.selection;
+		const tableUtils = this.editor.plugins.get( 'TableUtils' );
+
+		const position = selection.getFirstPosition();
+		const tableCell = position.parent;
+		const tableRow = tableCell.parent;
+		const table = tableRow.parent;
+
+		const currentHeadingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
+
+		const { column } = tableUtils.getCellLocation( tableCell );
+
+		const columnsToSet = column + 1 !== currentHeadingColumns ? column + 1 : column;
+
+		model.change( writer => {
+			updateNumericAttribute( 'headingColumns', columnsToSet, table, writer, 0 );
+		} );
+	}
+
+	/**
+	 * Checks if table cell is in heading section.
+	 *
+	 * @param {module:engine/model/element~Element} tableCell
+	 * @param {module:engine/model/element~Element} table
+	 * @returns {Boolean}
+	 * @private
+	 */
+	_isInHeading( tableCell, table ) {
+		const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
+
+		const tableUtils = this.editor.plugins.get( 'TableUtils' );
+
+		const { column } = tableUtils.getCellLocation( tableCell );
+
+		return !!headingColumns && column < headingColumns;
+	}
+}

+ 32 - 22
packages/ckeditor5-table/src/commands/settableheaderscommand.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module table/commands/settableheaderscommand
+ * @module table/commands/setheaderrowcommand
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
@@ -14,11 +14,11 @@ import { getParentTable, updateNumericAttribute } from './utils';
 import TableWalker from '../tablewalker';
 
 /**
- * The set table headers command.
+ * The header row command.
  *
  * @extends module:core/command~Command
  */
-export default class SetTableHeadersCommand extends Command {
+export default class SetHeaderRowCommand extends Command {
 	/**
 	 * @inheritDoc
 	 */
@@ -27,27 +27,34 @@ export default class SetTableHeadersCommand extends Command {
 		const doc = model.document;
 		const selection = doc.selection;
 
-		const tableParent = getParentTable( selection.getFirstPosition() );
+		const position = selection.getFirstPosition();
+		const tableParent = getParentTable( position );
 
 		this.isEnabled = !!tableParent;
+
+		this.value = this.isEnabled && this._isInHeading( position.parent, tableParent );
 	}
 
 	/**
 	 * @inheritDoc
 	 */
-	execute( options = {} ) {
+	execute() {
 		const model = this.editor.model;
 		const doc = model.document;
 		const selection = doc.selection;
 
-		const rowsToSet = parseInt( options.rows ) || 0;
+		const position = selection.getFirstPosition();
+		const tableCell = position.parent;
+		const tableRow = tableCell.parent;
+		const table = tableRow.parent;
 
-		const table = getParentTable( selection.getFirstPosition() );
+		const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
+		const rowIndex = tableRow.index;
 
-		model.change( writer => {
-			const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;
+		const rowsToSet = rowIndex + 1 !== currentHeadingRows ? rowIndex + 1 : rowIndex;
 
-			if ( currentHeadingRows !== rowsToSet && rowsToSet > 0 ) {
+		model.change( writer => {
+			if ( rowsToSet ) {
 				// Changing heading rows requires to check if any of a heading cell is overlaping vertically the table head.
 				// Any table cell that has a rowspan attribute > 1 will not exceed the table head so we need to fix it in rows below.
 				const cellsToSplit = getOverlappingCells( table, rowsToSet, currentHeadingRows );
@@ -57,11 +64,23 @@ export default class SetTableHeadersCommand extends Command {
 				}
 			}
 
-			const columnsToSet = parseInt( options.columns ) || 0;
-			updateTableAttribute( table, 'headingColumns', columnsToSet, writer );
-			updateTableAttribute( table, 'headingRows', rowsToSet, writer );
+			updateNumericAttribute( 'headingRows', rowsToSet, table, writer, 0 );
 		} );
 	}
+
+	/**
+	 * Checks if table cell is in heading section.
+	 *
+	 * @param {module:engine/model/element~Element} tableCell
+	 * @param {module:engine/model/element~Element} table
+	 * @returns {Boolean}
+	 * @private
+	 */
+	_isInHeading( tableCell, table ) {
+		const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
+
+		return headingRows && tableCell.parent.index < headingRows;
+	}
 }
 
 // Returns cells that span beyond new heading section.
@@ -86,15 +105,6 @@ function getOverlappingCells( table, headingRowsToSet, currentHeadingRows ) {
 	return cellsToSplit;
 }
 
-// @private
-function updateTableAttribute( table, attributeName, newValue, writer ) {
-	const currentValue = table.getAttribute( attributeName ) || 0;
-
-	if ( newValue !== currentValue ) {
-		updateNumericAttribute( attributeName, newValue, table, writer, 0 );
-	}
-}
-
 // Splits table cell horizontally.
 //
 // @param {module:engine/model/element~Element} tableCell

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

@@ -28,11 +28,12 @@ import SplitCellCommand from './commands/splitcellcommand';
 import MergeCellCommand from './commands/mergecellcommand';
 import RemoveRowCommand from './commands/removerowcommand';
 import RemoveColumnCommand from './commands/removecolumncommand';
-import SetTableHeadersCommand from './commands/settableheaderscommand';
+import SetHeaderRowCommand from './commands/setheaderrowcommand';
+import SetHeaderColumnCommand from './commands/setheadercolumncommand';
 import { getParentTable } from './commands/utils';
+import TableUtils from './tableutils';
 
 import './../theme/table.css';
-import TableUtils from './tableutils';
 
 /**
  * The table editing feature.
@@ -111,7 +112,8 @@ export default class TableEditing extends Plugin {
 		editor.commands.add( 'mergeCellDown', new MergeCellCommand( editor, { direction: 'down' } ) );
 		editor.commands.add( 'mergeCellUp', new MergeCellCommand( editor, { direction: 'up' } ) );
 
-		editor.commands.add( 'setTableHeaders', new SetTableHeadersCommand( editor ) );
+		editor.commands.add( 'setColumnHeader', new SetHeaderColumnCommand( editor ) );
+		editor.commands.add( 'setRowHeader', new SetHeaderRowCommand( editor ) );
 
 		// Handle tab key navigation.
 		this.listenTo( editor.editing.view.document, 'keydown', ( ...args ) => this._handleTabOnSelectedTable( ...args ) );

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

@@ -53,6 +53,7 @@ export default class TableUI extends Plugin {
 
 		editor.ui.componentFactory.add( 'tableColumn', locale => {
 			const options = [
+				{ command: 'setColumnHeader', label: 'Header column' },
 				{ command: 'insertColumnBefore', label: 'Insert column before' },
 				{ command: 'insertColumnAfter', label: 'Insert column after' },
 				{ command: 'removeColumn', label: 'Delete column' }
@@ -63,6 +64,7 @@ export default class TableUI extends Plugin {
 
 		editor.ui.componentFactory.add( 'tableRow', locale => {
 			const options = [
+				{ command: 'setRowHeader', label: 'Header row' },
 				{ command: 'insertRowBelow', label: 'Insert row below' },
 				{ command: 'insertRowAbove', label: 'Insert row above' },
 				{ command: 'removeRow', label: 'Delete row' }

+ 191 - 0
packages/ckeditor5-table/tests/commands/setheadercolumncommand.js

@@ -0,0 +1,191 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import SetHeaderColumnCommand from '../../src/commands/setheadercolumncommand';
+import {
+	downcastInsertCell,
+	downcastInsertRow,
+	downcastInsertTable,
+	downcastRemoveRow,
+	downcastTableHeadingColumnsChange,
+	downcastTableHeadingRowsChange
+} from '../../src/converters/downcast';
+import upcastTable from '../../src/converters/upcasttable';
+import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import TableUtils from '../../src/tableutils';
+
+describe( 'HeaderColumnCommand', () => {
+	let editor, model, command;
+
+	beforeEach( () => {
+		return ModelTestEditor.create( {
+			plugins: [ TableUtils ]
+		} ).then( newEditor => {
+			editor = newEditor;
+			model = editor.model;
+			command = new SetHeaderColumnCommand( editor );
+
+			const conversion = editor.conversion;
+			const schema = model.schema;
+
+			schema.register( 'table', {
+				allowWhere: '$block',
+				allowAttributes: [ 'headingRows' ],
+				isBlock: true,
+				isObject: true
+			} );
+
+			schema.register( 'tableRow', {
+				allowIn: 'table',
+				allowAttributes: [],
+				isBlock: true,
+				isLimit: true
+			} );
+
+			schema.register( 'tableCell', {
+				allowIn: 'tableRow',
+				allowContentOf: '$block',
+				allowAttributes: [ 'colspan', 'rowspan' ],
+				isBlock: true,
+				isLimit: true
+			} );
+
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+
+			// Table conversion.
+			conversion.for( 'upcast' ).add( upcastTable() );
+			conversion.for( 'downcast' ).add( downcastInsertTable() );
+
+			// Insert row conversion.
+			conversion.for( 'downcast' ).add( downcastInsertRow() );
+
+			// Remove row conversion.
+			conversion.for( 'downcast' ).add( downcastRemoveRow() );
+
+			// Table cell conversion.
+			conversion.for( 'downcast' ).add( downcastInsertCell() );
+
+			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
+			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
+
+			// Table attributes conversion.
+			conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
+			conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+
+			conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
+			conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+		} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'should be false if selection is not in a table', () => {
+			setData( model, '<p>foo[]</p>' );
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be true if selection is in table', () => {
+			setData( model, '<table><tableRow><tableCell>foo[]</tableCell></tableRow></table>' );
+			expect( command.isEnabled ).to.be.true;
+		} );
+	} );
+
+	describe( 'value', () => {
+		it( 'should be false if selection is not in a heading column', () => {
+			setData( model, modelTable( [
+				[ '01', '02' ],
+				[ '11', '12[]' ]
+			], { headingColumns: 1 } ) );
+
+			expect( command.value ).to.be.false;
+		} );
+
+		it( 'should be true if selection is in a heading column', () => {
+			setData( model, modelTable( [
+				[ '01[]', '02' ],
+				[ '11', '12' ]
+			], { headingColumns: 1 } ) );
+
+			expect( command.value ).to.be.true;
+		} );
+
+		it( 'should be false if selection is in a heading row', () => {
+			setData( model, modelTable( [
+				[ '01', '02[]' ],
+				[ '11', '12' ]
+			], { headingRows: 1, headingColumns: 1 } ) );
+
+			expect( command.value ).to.be.false;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should set heading columns attribute that cover column in which is selection', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '[]10', '11' ],
+				[ '20', '21' ]
+			] ) );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '00', '01' ],
+				[ '[]10', '11' ],
+				[ '20', '21' ]
+			], { headingColumns: 1 } ) );
+		} );
+
+		it(
+			'should set heading columns attribute if currently selected column is a heading so the heading section is before this column',
+			() => {
+				setData( model, modelTable( [
+					[ '00', '01' ],
+					[ '[]10', '11' ],
+					[ '20', '21' ]
+				], { headingColumns: 2 } ) );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '00', '01' ],
+					[ '[]10', '11' ],
+					[ '20', '21' ]
+				], { headingColumns: 1 } ) );
+			}
+		);
+
+		it( 'should toggle of selected column', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '10', '11[]' ],
+				[ '20', '21' ]
+			], { headingColumns: 2 } ) );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '00', '01' ],
+				[ '10', '11[]' ],
+				[ '20', '21' ]
+			], { headingColumns: 1 } ) );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ '00', '01' ],
+				[ '10', '11[]' ],
+				[ '20', '21' ]
+			], { headingColumns: 2 } ) );
+		} );
+	} );
+} );

+ 96 - 89
packages/ckeditor5-table/tests/commands/settableheaderscommand.js

@@ -6,8 +6,8 @@
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import SetHeaderRowCommand from '../../src/commands/setheaderrowcommand';
 
-import SetTableHeadersCommand from '../../src/commands/settableheaderscommand';
 import {
 	downcastInsertCell,
 	downcastInsertRow,
@@ -18,60 +18,62 @@ import {
 } from '../../src/converters/downcast';
 import upcastTable from '../../src/converters/upcasttable';
 import { formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import TableUtils from '../../src/tableutils';
 
-describe( 'SetTableHeadersCommand', () => {
+describe( 'HeaderRowCommand', () => {
 	let editor, model, command;
 
 	beforeEach( () => {
-		return ModelTestEditor.create()
-			.then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				command = new SetTableHeadersCommand( editor );
-
-				const conversion = editor.conversion;
-				const schema = model.schema;
-
-				schema.register( 'table', {
-					allowWhere: '$block',
-					allowAttributes: [ 'headingRows' ],
-					isObject: true
-				} );
+		return ModelTestEditor.create( {
+			plugins: [ TableUtils ]
+		} ).then( newEditor => {
+			editor = newEditor;
+			model = editor.model;
+			command = new SetHeaderRowCommand( editor );
+
+			const conversion = editor.conversion;
+			const schema = model.schema;
+
+			schema.register( 'table', {
+				allowWhere: '$block',
+				allowAttributes: [ 'headingRows' ],
+				isObject: true
+			} );
 
-				schema.register( 'tableRow', { allowIn: 'table' } );
+			schema.register( 'tableRow', { allowIn: 'table' } );
 
-				schema.register( 'tableCell', {
-					allowIn: 'tableRow',
-					allowContentOf: '$block',
-					allowAttributes: [ 'colspan', 'rowspan' ],
-					isLimit: true
-				} );
+			schema.register( 'tableCell', {
+				allowIn: 'tableRow',
+				allowContentOf: '$block',
+				allowAttributes: [ 'colspan', 'rowspan' ],
+				isLimit: true
+			} );
 
-				model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
 
-				// Table conversion.
-				conversion.for( 'upcast' ).add( upcastTable() );
-				conversion.for( 'downcast' ).add( downcastInsertTable() );
+			// Table conversion.
+			conversion.for( 'upcast' ).add( upcastTable() );
+			conversion.for( 'downcast' ).add( downcastInsertTable() );
 
-				// Insert row conversion.
-				conversion.for( 'downcast' ).add( downcastInsertRow() );
+			// Insert row conversion.
+			conversion.for( 'downcast' ).add( downcastInsertRow() );
 
-				// Remove row conversion.
-				conversion.for( 'downcast' ).add( downcastRemoveRow() );
+			// Remove row conversion.
+			conversion.for( 'downcast' ).add( downcastRemoveRow() );
 
-				// Table cell conversion.
-				conversion.for( 'downcast' ).add( downcastInsertCell() );
+			// Table cell conversion.
+			conversion.for( 'downcast' ).add( downcastInsertCell() );
 
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
-				conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
+			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'td' } ) );
+			conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableCell', view: 'th' } ) );
 
-				// Table attributes conversion.
-				conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
-				conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
+			// Table attributes conversion.
+			conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
+			conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
 
-				conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
-				conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
-			} );
+			conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange() );
+			conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange() );
+		} );
 	} );
 
 	afterEach( () => {
@@ -79,88 +81,93 @@ describe( 'SetTableHeadersCommand', () => {
 	} );
 
 	describe( 'isEnabled', () => {
-		it( 'should be false if not in a table', () => {
+		it( 'should be false if selection is not in a table', () => {
 			setData( model, '<p>foo[]</p>' );
 			expect( command.isEnabled ).to.be.false;
 		} );
 
-		it( 'should be true if in table', () => {
+		it( 'should be true if selection is in table', () => {
 			setData( model, '<table><tableRow><tableCell>foo[]</tableCell></tableRow></table>' );
 			expect( command.isEnabled ).to.be.true;
 		} );
 	} );
 
-	describe( 'execute()', () => {
-		it( 'should set heading rows attribute', () => {
+	describe( 'value', () => {
+		it( 'should be false if selection is not in a heading row', () => {
 			setData( model, modelTable( [
-				[ '00', '01' ],
-				[ '[]10', '11' ],
-				[ '20', '21' ]
-			] ) );
-
-			command.execute( { rows: 2 } );
+				[ '01', '02' ],
+				[ '11', '12[]' ]
+			], { headingRows: 1 } ) );
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '00', '01' ],
-				[ '[]10', '11' ],
-				[ '20', '21' ]
-			], { headingRows: 2 } ) );
+			expect( command.value ).to.be.false;
 		} );
 
-		it( 'should remove heading rows attribute', () => {
+		it( 'should be true if selection is in a heading row', () => {
 			setData( model, modelTable( [
-				[ '00', '01' ],
-				[ '[]10', '11' ],
-				[ '20', '21' ]
-			], { headingRows: 2 } ) );
+				[ '01[]', '02' ],
+				[ '11', '12' ]
+			], { headingRows: 1 } ) );
 
-			command.execute( { rows: 0 } );
+			expect( command.value ).to.be.true;
+		} );
 
-			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '00', '01' ],
-				[ '[]10', '11' ],
-				[ '20', '21' ]
-			] ) );
+		it( 'should be false if selection is in a heading column', () => {
+			setData( model, modelTable( [
+				[ '01', '02' ],
+				[ '11[]', '12' ]
+			], { headingRows: 1, headingColumns: 1 } ) );
+
+			expect( command.value ).to.be.false;
 		} );
+	} );
 
-		it( 'should set heading columns attribute', () => {
+	describe( 'execute()', () => {
+		it( 'should set heading rows attribute that cover row in which is selection', () => {
 			setData( model, modelTable( [
 				[ '00', '01' ],
 				[ '[]10', '11' ],
 				[ '20', '21' ]
 			] ) );
 
-			command.execute( { columns: 2 } );
+			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ '00', '01' ],
 				[ '[]10', '11' ],
 				[ '20', '21' ]
-			], { headingColumns: 2 } ) );
+			], { headingRows: 2 } ) );
 		} );
 
-		it( 'should remove heading columns attribute', () => {
+		it( 'should toggle heading rows attribute', () => {
 			setData( model, modelTable( [
-				[ '00', '01' ],
-				[ '[]10', '11' ],
+				[ '[]00', '01' ],
+				[ '10', '11' ],
 				[ '20', '21' ]
-			], { headingColumns: 2 } ) );
+			] ) );
 
-			command.execute( { columns: 0 } );
+			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-				[ '00', '01' ],
-				[ '[]10', '11' ],
+				[ '[]00', '01' ],
+				[ '10', '11' ],
+				[ '20', '21' ]
+			], { headingRows: 1 } ) );
+
+			command.execute();
+
+			setData( model, modelTable( [
+				[ '[]00', '01' ],
+				[ '10', '11' ],
 				[ '20', '21' ]
 			] ) );
 		} );
 
-		it( 'should remove heading columns & heading rows attributes', () => {
+		it( 'should set heading rows attribute if currently selected row is a heading so the heading section is below this row', () => {
 			setData( model, modelTable( [
 				[ '00', '01' ],
 				[ '[]10', '11' ],
 				[ '20', '21' ]
-			], { headingColumns: 2, headingRows: 2 } ) );
+			], { headingRows: 2 } ) );
 
 			command.execute();
 
@@ -168,7 +175,7 @@ describe( 'SetTableHeadersCommand', () => {
 				[ '00', '01' ],
 				[ '[]10', '11' ],
 				[ '20', '21' ]
-			] ) );
+			], { headingRows: 1 } ) );
 		} );
 
 		it( 'should fix rowspaned cells on the edge of an table head section', () => {
@@ -178,7 +185,7 @@ describe( 'SetTableHeadersCommand', () => {
 				[ '22' ]
 			], { headingColumns: 2, headingRows: 1 } ) );
 
-			command.execute( { rows: 2, columns: 2 } );
+			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ '00', '01', '02' ],
@@ -190,19 +197,19 @@ describe( 'SetTableHeadersCommand', () => {
 		it( 'should split to at most 2 table cells when fixing rowspaned cells on the edge of an table head section', () => {
 			setData( model, modelTable( [
 				[ '00', '01', '02' ],
-				[ { colspan: 2, rowspan: 5, contents: '10[]' }, '12' ],
-				[ '22' ],
+				[ { colspan: 2, rowspan: 5, contents: '10' }, '12' ],
+				[ '22[]' ],
 				[ '32' ],
 				[ '42' ],
 				[ '52' ]
 			], { headingColumns: 2, headingRows: 1 } ) );
 
-			command.execute( { rows: 3, columns: 2 } );
+			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ '00', '01', '02' ],
-				[ { colspan: 2, rowspan: 2, contents: '10[]' }, '12' ],
-				[ '22' ],
+				[ { colspan: 2, rowspan: 2, contents: '10' }, '12' ],
+				[ '22[]' ],
 				[ { colspan: 2, rowspan: 3, contents: '' }, '32' ],
 				[ '42' ],
 				[ '52' ]
@@ -215,11 +222,11 @@ describe( 'SetTableHeadersCommand', () => {
 				[ '11' ]
 			], { headingRows: 2 } ) );
 
-			command.execute( { rows: 1 } );
+			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ '[]00', '01' ],
-				[ '', '11' ],
+				[ '', '11' ]
 			], { headingRows: 1 } ) );
 		} );
 
@@ -229,7 +236,7 @@ describe( 'SetTableHeadersCommand', () => {
 				[ '10' ]
 			], { headingRows: 2 } ) );
 
-			command.execute( { rows: 1 } );
+			command.execute();
 
 			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
 				[ '00', '[]01' ],

+ 8 - 3
packages/ckeditor5-table/tests/tableediting.js

@@ -17,7 +17,8 @@ import RemoveRowCommand from '../src/commands/removerowcommand';
 import RemoveColumnCommand from '../src/commands/removecolumncommand';
 import SplitCellCommand from '../src/commands/splitcellcommand';
 import MergeCellCommand from '../src/commands/mergecellcommand';
-import SetTableHeadersCommand from '../src/commands/settableheaderscommand';
+import SetHeaderRowCommand from '../src/commands/setheaderrowcommand';
+import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand';
 
 describe( 'TableEditing', () => {
 	let editor, model;
@@ -93,8 +94,12 @@ describe( 'TableEditing', () => {
 		expect( editor.commands.get( 'mergeCellUp' ) ).to.be.instanceOf( MergeCellCommand );
 	} );
 
-	it( 'adds setTableHeaders command', () => {
-		expect( editor.commands.get( 'setTableHeaders' ) ).to.be.instanceOf( SetTableHeadersCommand );
+	it( 'adds setColumnHeader command', () => {
+		expect( editor.commands.get( 'setColumnHeader' ) ).to.be.instanceOf( SetHeaderColumnCommand );
+	} );
+
+	it( 'adds setRowHeader command', () => {
+		expect( editor.commands.get( 'setRowHeader' ) ).to.be.instanceOf( SetHeaderRowCommand );
 	} );
 
 	describe( 'conversion in data pipeline', () => {

+ 25 - 11
packages/ckeditor5-table/tests/tableui.js

@@ -105,16 +105,18 @@ describe( 'TableUI', () => {
 
 			const labels = listView.items.map( ( { label } ) => label );
 
-			expect( labels ).to.deep.equal( [ 'Insert row below', 'Insert row above', 'Delete row' ] );
+			expect( labels ).to.deep.equal( [ 'Header row', 'Insert row below', 'Insert row above', 'Delete row' ] );
 		} );
 
 		it( 'should bind items in panel to proper commands', () => {
 			const items = dropdown.listView.items;
 
+			const setRowHeaderCommand = editor.commands.get( 'setRowHeader' );
 			const insertRowBelowCommand = editor.commands.get( 'insertRowBelow' );
 			const insertRowAboveCommand = editor.commands.get( 'insertRowAbove' );
 			const removeRowCommand = editor.commands.get( 'removeRow' );
 
+			setRowHeaderCommand.isEnabled = true;
 			insertRowBelowCommand.isEnabled = true;
 			insertRowAboveCommand.isEnabled = true;
 			removeRowCommand.isEnabled = true;
@@ -122,20 +124,26 @@ describe( 'TableUI', () => {
 			expect( items.get( 0 ).isEnabled ).to.be.true;
 			expect( items.get( 1 ).isEnabled ).to.be.true;
 			expect( items.get( 2 ).isEnabled ).to.be.true;
+			expect( items.get( 3 ).isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
-			insertRowBelowCommand.isEnabled = false;
+			setRowHeaderCommand.isEnabled = false;
 
 			expect( items.get( 0 ).isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
-			insertRowAboveCommand.isEnabled = false;
+			insertRowBelowCommand.isEnabled = false;
 
 			expect( items.get( 1 ).isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
-			removeRowCommand.isEnabled = false;
+			insertRowAboveCommand.isEnabled = false;
 			expect( items.get( 2 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			removeRowCommand.isEnabled = false;
+
+			expect( items.get( 3 ).isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 		} );
 
@@ -153,7 +161,7 @@ describe( 'TableUI', () => {
 			dropdown.listView.items.get( 0 ).fire( 'execute' );
 
 			expect( spy.calledOnce ).to.be.true;
-			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'insertRowBelow' );
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'setRowHeader' );
 		} );
 	} );
 
@@ -180,16 +188,18 @@ describe( 'TableUI', () => {
 
 			const labels = listView.items.map( ( { label } ) => label );
 
-			expect( labels ).to.deep.equal( [ 'Insert column before', 'Insert column after', 'Delete column' ] );
+			expect( labels ).to.deep.equal( [ 'Header column', 'Insert column before', 'Insert column after', 'Delete column' ] );
 		} );
 
 		it( 'should bind items in panel to proper commands', () => {
 			const items = dropdown.listView.items;
 
+			const setColumnHeaderCommand = editor.commands.get( 'setColumnHeader' );
 			const insertColumnBeforeCommand = editor.commands.get( 'insertColumnBefore' );
 			const insertColumnAfterCommand = editor.commands.get( 'insertColumnAfter' );
 			const removeColumnCommand = editor.commands.get( 'removeColumn' );
 
+			setColumnHeaderCommand.isEnabled = true;
 			insertColumnBeforeCommand.isEnabled = true;
 			insertColumnAfterCommand.isEnabled = true;
 			removeColumnCommand.isEnabled = true;
@@ -197,24 +207,28 @@ describe( 'TableUI', () => {
 			expect( items.get( 0 ).isEnabled ).to.be.true;
 			expect( items.get( 1 ).isEnabled ).to.be.true;
 			expect( items.get( 2 ).isEnabled ).to.be.true;
+			expect( items.get( 3 ).isEnabled ).to.be.true;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
-			insertColumnBeforeCommand.isEnabled = false;
+			setColumnHeaderCommand.isEnabled = false;
 
 			expect( items.get( 0 ).isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
-			insertColumnAfterCommand.isEnabled = false;
+			insertColumnBeforeCommand.isEnabled = false;
 
 			expect( items.get( 1 ).isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.true;
 
-			removeColumnCommand.isEnabled = false;
+			insertColumnAfterCommand.isEnabled = false;
 			expect( items.get( 2 ).isEnabled ).to.be.false;
+
+			removeColumnCommand.isEnabled = false;
+			expect( items.get( 3 ).isEnabled ).to.be.false;
 			expect( dropdown.buttonView.isEnabled ).to.be.false;
 		} );
 
-		it( 'should focus view after command execution', () => {
+		it( 'should focus view a+fter command execution', () => {
 			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
 
 			dropdown.listView.items.get( 0 ).fire( 'execute' );
@@ -228,7 +242,7 @@ describe( 'TableUI', () => {
 			dropdown.listView.items.get( 0 ).fire( 'execute' );
 
 			expect( spy.calledOnce ).to.be.true;
-			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'insertColumnBefore' );
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'setColumnHeader' );
 		} );
 	} );