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

Merge remote-tracking branch 'origin/i/3287' into i/6049

Aleksander Nowodzinski 6 лет назад
Родитель
Сommit
461eb46b20
48 измененных файлов с 5508 добавлено и 1713 удалено
  1. 14 0
      packages/ckeditor5-table/src/commands/utils.js
  2. 35 7
      packages/ckeditor5-table/src/converters/tableproperties.js
  3. 7 94
      packages/ckeditor5-table/src/tablecellproperties.js
  4. 35 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbackgroundcolorcommand.js
  5. 47 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbordercolorcommand.js
  6. 47 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderstylecommand.js
  7. 47 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderwidthcommand.js
  8. 35 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellheightcommand.js
  9. 35 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js
  10. 47 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpaddingcommand.js
  11. 88 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js
  12. 42 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js
  13. 35 0
      packages/ckeditor5-table/src/tablecellproperties/commands/tablecellwidthcommand.js
  14. 164 0
      packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js
  15. 0 46
      packages/ckeditor5-table/src/tablecolumnrowproperties.js
  16. 5 118
      packages/ckeditor5-table/src/tableproperties.js
  17. 35 0
      packages/ckeditor5-table/src/tableproperties/commands/tablealignmentcommand.js
  18. 35 0
      packages/ckeditor5-table/src/tableproperties/commands/tablebackgroundcolorcommand.js
  19. 47 0
      packages/ckeditor5-table/src/tableproperties/commands/tablebordercolorcommand.js
  20. 47 0
      packages/ckeditor5-table/src/tableproperties/commands/tableborderstylecommand.js
  21. 47 0
      packages/ckeditor5-table/src/tableproperties/commands/tableborderwidthcommand.js
  22. 35 0
      packages/ckeditor5-table/src/tableproperties/commands/tableheightcommand.js
  23. 87 0
      packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js
  24. 35 0
      packages/ckeditor5-table/src/tableproperties/commands/tablewidthcommand.js
  25. 176 0
      packages/ckeditor5-table/src/tableproperties/tablepropertiesediting.js
  26. 67 1
      packages/ckeditor5-table/tests/_utils/utils.js
  27. 3 7
      packages/ckeditor5-table/tests/manual/tableproperties.js
  28. 23 670
      packages/ckeditor5-table/tests/tablecellproperties.js
  29. 153 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbackgroundcolorcommand.js
  30. 178 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbordercolorcommand.js
  31. 178 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderstylecommand.js
  32. 178 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderwidthcommand.js
  33. 153 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellheightcommand.js
  34. 153 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js
  35. 178 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellpaddingcommand.js
  36. 153 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellverticalalignmentcommand.js
  37. 153 0
      packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellwidthcommand.js
  38. 828 0
      packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js
  39. 0 123
      packages/ckeditor5-table/tests/tablecolumnrowproperties.js
  40. 21 647
      packages/ckeditor5-table/tests/tableproperties.js
  41. 153 0
      packages/ckeditor5-table/tests/tableproperties/commands/tablealignmentcommand.js
  42. 153 0
      packages/ckeditor5-table/tests/tableproperties/commands/tablebackgroundcolorcommand.js
  43. 178 0
      packages/ckeditor5-table/tests/tableproperties/commands/tablebordercolorcommand.js
  44. 178 0
      packages/ckeditor5-table/tests/tableproperties/commands/tableborderstylecommand.js
  45. 178 0
      packages/ckeditor5-table/tests/tableproperties/commands/tableborderwidthcommand.js
  46. 153 0
      packages/ckeditor5-table/tests/tableproperties/commands/tableheightcommand.js
  47. 153 0
      packages/ckeditor5-table/tests/tableproperties/commands/tablewidthcommand.js
  48. 716 0
      packages/ckeditor5-table/tests/tableproperties/tablepropertiesediting.js

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

@@ -7,6 +7,8 @@
  * @module table/commands/utils
  */
 
+import { isObject } from 'lodash-es';
+
 /**
  * Returns the parent element of given name. Returns undefined if position is not inside desired parent.
  *
@@ -55,3 +57,15 @@ export function createEmptyTableCell( writer, insertPosition, attributes = {} )
 	writer.insertElement( 'paragraph', tableCell );
 	writer.insert( tableCell, insertPosition );
 }
+
+export function getSingleValue( objectOrString ) {
+	if ( !objectOrString || !isObject( objectOrString ) ) {
+		return objectOrString;
+	}
+
+	const { top, right, bottom, left } = objectOrString;
+
+	if ( top == right && right == bottom && bottom == left ) {
+		return top;
+	}
+}

+ 35 - 7
packages/ckeditor5-table/src/tableproperties/utils.js → packages/ckeditor5-table/src/converters/tableproperties.js

@@ -1,13 +1,21 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @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/tableproperites/utils
+ * @module table/converters/tableproperites
  */
 
-export function upcastAttribute( conversion, modelElement, modelAttribute, styleName ) {
+/**
+ * Conversion helper for upcasting attribute using normalized styles.
+ *
+ * @param {module:engine/conversion/conversion~Conversion} conversion
+ * @param {String} modelElement
+ * @param {String} modelAttribute
+ * @param {String} styleName
+ */
+export function upcastStyleToAttribute( conversion, modelElement, modelAttribute, styleName ) {
 	conversion.for( 'upcast' ).attributeToAttribute( {
 		view: {
 			styles: {
@@ -22,8 +30,14 @@ export function upcastAttribute( conversion, modelElement, modelAttribute, style
 	} );
 }
 
-export function upcastBorderStyles( conversion, viewElement ) {
-	conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:' + viewElement, ( evt, data, conversionApi ) => {
+/**
+ * Conversion helper for upcasting border styles for view element.
+ *
+ * @param {module:engine/conversion/conversion~Conversion} conversion
+ * @param {String} viewElementName
+ */
+export function upcastBorderStyles( conversion, viewElementName ) {
+	conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:' + viewElementName, ( evt, data, conversionApi ) => {
 		// TODO: this is counter-intuitive: ie.: if only `border-top` is defined then `hasStyle( 'border' )` also returns true.
 		// TODO: this might needs to be fixed in styles normalizer.
 		const stylesToConsume = [
@@ -56,7 +70,15 @@ export function upcastBorderStyles( conversion, viewElement ) {
 	} ) );
 }
 
-export function downcastToStyle( conversion, modelElement, modelAttribute, styleName ) {
+/**
+ * Conversion helper for downcasting attribute to a style.
+ *
+ * @param {module:engine/conversion/conversion~Conversion} conversion
+ * @param {String} modelElement
+ * @param {String} modelAttribute
+ * @param {String} styleName
+ */
+export function downcastAttributeToStyle( conversion, modelElement, modelAttribute, styleName ) {
 	conversion.for( 'downcast' ).attributeToAttribute( {
 		model: {
 			name: modelElement,
@@ -71,7 +93,13 @@ export function downcastToStyle( conversion, modelElement, modelAttribute, style
 	} );
 }
 
-// Properly downcast table border attribute on <table> and not on <figure>.
+/**
+ * Conversion helper for downcasting attributes from model's table to a view table (not to figure).
+ *
+ * @param {module:engine/conversion/conversion~Conversion} conversion
+ * @param {String} modelAttribute
+ * @param {String} styleName
+ */
 export function downcastTableAttribute( conversion, modelAttribute, styleName ) {
 	conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( `attribute:${ modelAttribute }:table`, ( evt, data, conversionApi ) => {
 		const { item, attributeNewValue } = data;

+ 7 - 94
packages/ckeditor5-table/src/tablecellproperties.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
@@ -9,11 +9,15 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import TableCellPropertiesUI from './tablecellpropertiesui';
-import { downcastToStyle, upcastAttribute, upcastBorderStyles } from './tableproperties/utils';
+import TableCellPropertiesEditing from './tablecellproperties/tablecellpropertiesediting';
 
 /**
  * The table cell properties feature.
  *
+ * This is a "glue" plugin which loads the
+ * {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing table editing feature} and
+ * the {@link module:table/tablecellproperties/tablecellpropertiesui~TableCellPropertiesUI table UI feature}.
+ *
  * @extends module:core/plugin~Plugin
  */
 export default class TableCellProperties extends Plugin {
@@ -28,97 +32,6 @@ export default class TableCellProperties extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ TableCellPropertiesUI ];
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	afterInit() {
-		const editor = this.editor;
-		const schema = editor.model.schema;
-		const conversion = editor.conversion;
-
-		this.enableBorderProperties( schema, conversion );
-		this.enableBackgroundColorProperty( schema, conversion );
-		this.enablePaddingProperty( schema, conversion );
-		this.enableVerticalAlignmentProperty( schema, conversion );
-		this.enableHorizontalAlignmentProperty( schema, conversion );
-	}
-
-	enableBorderProperties( schema, conversion ) {
-		schema.extend( 'tableCell', {
-			allowAttributes: [ 'borderWidth', 'borderColor', 'borderStyle' ]
-		} );
-		upcastBorderStyles( conversion, 'td' );
-		upcastBorderStyles( conversion, 'th' );
-		downcastToStyle( conversion, 'tableCell', 'borderStyle', 'border-style' );
-		downcastToStyle( conversion, 'tableCell', 'borderColor', 'border-color' );
-		downcastToStyle( conversion, 'tableCell', 'borderWidth', 'border-width' );
-	}
-
-	enableBackgroundColorProperty( schema, conversion ) {
-		schema.extend( 'tableCell', {
-			allowAttributes: [ 'backgroundColor' ]
-		} );
-		upcastAttribute( conversion, 'tableCell', 'backgroundColor', 'background-color' );
-		downcastToStyle( conversion, 'tableCell', 'backgroundColor', 'background-color' );
-	}
-
-	enablePaddingProperty( schema, conversion ) {
-		schema.extend( 'tableCell', {
-			allowAttributes: [ 'padding' ]
-		} );
-		upcastAttribute( conversion, 'tableCell', 'padding', 'padding' );
-		downcastToStyle( conversion, 'tableCell', 'padding', 'padding' );
-	}
-
-	enableVerticalAlignmentProperty( schema, conversion ) {
-		schema.extend( 'tableCell', {
-			allowAttributes: [ 'verticalAlignment' ]
-		} );
-		upcastAttribute( conversion, 'tableCell', 'verticalAlignment', 'vertical-align' );
-		downcastToStyle( conversion, 'tableCell', 'verticalAlignment', 'vertical-align' );
-	}
-
-	enableHorizontalAlignmentProperty( schema, conversion ) {
-		schema.extend( 'tableCell', {
-			allowAttributes: [ 'horizontalAlignment' ]
-		} );
-
-		conversion.attributeToAttribute( {
-			model: {
-				name: 'tableCell',
-				key: 'horizontalAlignment',
-				values: [ 'left', 'right', 'center', 'justify' ]
-			},
-			view: {
-				// TODO: controversial one but I don't know if we want "default".
-				left: {
-					key: 'style',
-					value: {
-						'text-align': 'left'
-					}
-				},
-				right: {
-					key: 'style',
-					value: {
-						'text-align': 'right'
-					}
-				},
-				center: {
-					key: 'style',
-					value: {
-						'text-align': 'center'
-					}
-				},
-				justify: {
-					key: 'style',
-					value: {
-						'text-align': 'justify'
-					}
-				}
-			}
-		} );
+		return [ TableCellPropertiesEditing, TableCellPropertiesUI ];
 	}
 }

+ 35 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbackgroundcolorcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tablecellproperties/commands/tablecellbackgroundcolorcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+
+/**
+ * The table cell background color command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellBackgroundColor'` editor command.
+ *
+ * To change cell `backgroundColor` attribute of the selected cells, execute the command:
+ *
+ *		editor.execute( 'tableCellBackgroundColor', {
+ *			value: '#f00'
+ *		} );
+ *
+ * @extends module:core/command~Command
+ */
+export default class TableCellBackgroundColorCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellBackgroundColorCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'backgroundColor' );
+	}
+}

+ 47 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellbordercolorcommand.js

@@ -0,0 +1,47 @@
+/**
+ * @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/tablecellproperties/commands/tablecellbordercolorcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+import { getSingleValue } from '../../commands/utils';
+
+/**
+ * The table cell border color command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellBorderColor'` editor command.
+ *
+ * To change cell border color of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellBorderColor', {
+ *			value: '#f00'
+ *		} );
+ *
+ * @extends module:core/command~Command
+ */
+export default class TableCellBorderColorCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellBorderWidthCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'borderColor' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getAttribute( tableCell ) {
+		if ( !tableCell ) {
+			return;
+		}
+
+		return getSingleValue( tableCell.getAttribute( this.attributeName ) );
+	}
+}

+ 47 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderstylecommand.js

@@ -0,0 +1,47 @@
+/**
+ * @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/tablecellproperties/commands/tablecellborderstylecommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+import { getSingleValue } from '../../commands/utils';
+
+/**
+ * The table cell border style command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellBorderStyle'` editor command.
+ *
+ * To change cell border style of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellBorderStyle', {
+ *			value: 'dashed'
+ *		} );
+ *
+ * @extends module:core/command~Command
+ */
+export default class TableCellBorderStyleCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellBorderWidthCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'borderStyle' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getAttribute( tableCell ) {
+		if ( !tableCell ) {
+			return;
+		}
+
+		return getSingleValue( tableCell.getAttribute( this.attributeName ) );
+	}
+}

+ 47 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellborderwidthcommand.js

@@ -0,0 +1,47 @@
+/**
+ * @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/tablecellproperties/commands/tablecellborderwidthcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+import { getSingleValue } from '../../commands/utils';
+
+/**
+ * The table cell border width command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellBorderWidth'` editor command.
+ *
+ * To change cell border width of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellBorderWidth', {
+ *			value: '5px'
+ *		} );
+ *
+ * @extends module:core/command~Command
+ */
+export default class TableCellBorderWidthCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellBorderWidthCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'borderWidth' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getAttribute( tableCell ) {
+		if ( !tableCell ) {
+			return;
+		}
+
+		return getSingleValue( tableCell.getAttribute( this.attributeName ) );
+	}
+}

+ 35 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellheightcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tablecellproperties/commands/tablecellheightcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+
+/**
+ * The table cell height command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellHeight'` editor command.
+ *
+ * To change cell height of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellHeight', {
+ *			value: '50px'
+ *		} );
+ *
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ */
+export default class TableCellHeightCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellHeightCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'height' );
+	}
+}

+ 35 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tablecellproperties/commands/tablecellhorizontalalignmentcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+
+/**
+ * The table cell horizontal alignment command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellHorizontalAlignment'` editor command.
+ *
+ * To change cell horizontal alignment of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellHorizontalAlignment', {
+ *			value: 'right'
+ *		} );
+ *
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ */
+export default class TableCellHorizontalAlignmentCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellHorizontalAlignmentCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'horizontalAlignment' );
+	}
+}

+ 47 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpaddingcommand.js

@@ -0,0 +1,47 @@
+/**
+ * @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/tablecellproperties/commands/tablecellpaddingcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+import { getSingleValue } from '../../commands/utils';
+
+/**
+ * The table cell padding command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellPadding'` editor command.
+ *
+ * To change cell padding of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellPadding', {
+ *			value: '5px'
+ *		} );
+ *
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ */
+export default class TableCellPaddingCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellPaddingCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'padding' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getAttribute( tableCell ) {
+		if ( !tableCell ) {
+			return;
+		}
+
+		return getSingleValue( tableCell.getAttribute( this.attributeName ) );
+	}
+}

+ 88 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js

@@ -0,0 +1,88 @@
+/**
+ * @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/tablecellproperties/commands/tablecellpropertycommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+import { findAncestor } from '../../commands/utils';
+
+/**
+ * The table cell attribute command.
+ *
+ * The command is a base command for other table cell attributes commands.
+ *
+ * @extends module:core/command~Command
+ */
+export default class TableCellPropertyCommand extends Command {
+	/**
+	 * Creates a new `TableCellPropertyCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 * @param {String} attributeName Table cell attribute name.
+	 */
+	constructor( editor, attributeName ) {
+		super( editor );
+
+		this.attributeName = attributeName;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const editor = this.editor;
+		const selection = editor.model.document.selection;
+
+		const tableCell = findAncestor( 'tableCell', selection.getFirstPosition() );
+
+		this.isEnabled = !!tableCell;
+		this.value = this._getAttribute( tableCell );
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 * @fires execute
+	 * @param {Object} [options]
+	 * @param {*} [options.value] If set the command will set the attribute on selected table cells.
+	 * If it is not set the command will remove the attribute from selected table cells.
+	 * @param {module:engine/model/batch~Batch} [options.batch] Pass batch instance to the command for creating single undo step.
+	 */
+	execute( options = {} ) {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+
+		const { value, batch } = options;
+
+		const tableCells = Array.from( selection.getSelectedBlocks() )
+			.map( element => findAncestor( 'tableCell', model.createPositionAt( element, 0 ) ) );
+
+		model.enqueueChange( batch || 'default', writer => {
+			if ( value ) {
+				tableCells.forEach( tableCell => writer.setAttribute( this.attributeName, value, tableCell ) );
+			} else {
+				tableCells.forEach( tableCell => writer.removeAttribute( this.attributeName, tableCell ) );
+			}
+		} );
+	}
+
+	/**
+	 * Returns attribute value for a table cell.
+	 *
+	 * @param {module:engine/model/element~Element} tableCell
+	 * @returns {String|undefined}
+	 * @private
+	 */
+	_getAttribute( tableCell ) {
+		if ( !tableCell ) {
+			return;
+		}
+
+		return tableCell.getAttribute( this.attributeName );
+	}
+}

+ 42 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellverticalalignmentcommand.js

@@ -0,0 +1,42 @@
+/**
+ * @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/tablecellproperties/commands/tablecellverticalalignmentcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+
+/**
+ * The table cell vertical alignment command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellVerticalAlignment'` editor command.
+ *
+ * To change cell vertical alignment of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellVerticalAlignment', {
+ *			value: 'top'
+ *		} );
+ *
+ * The editor UI allows to set those attributes from
+ * [a `vertical-align` CSS attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align):
+ *
+ * * `'top'`
+ * * `'bottom'`
+ * * `'middle'`
+ *
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ */
+export default class TableCellVerticalAlignmentCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellVerticalAlignmentCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'verticalAlignment' );
+	}
+}

+ 35 - 0
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellwidthcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tablecellproperties/commands/tablecellwidthcommand
+ */
+
+import TableCellPropertyCommand from './tablecellpropertycommand';
+
+/**
+ * The table cell width command.
+ *
+ * The command is registered by {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
+ * `'tableCellWidth'` editor command.
+ *
+ * To change cell width of the selected cell, execute the command:
+ *
+ *		editor.execute( 'tableCellWidth', {
+ *			value: '50px'
+ *		} );
+ *
+ * @extends module:table/tablecellproperties/commands/tablecellpropertycommand
+ */
+export default class TableCellWidthCommand extends TableCellPropertyCommand {
+	/**
+	 * Creates a new `TableCellWidthCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'width' );
+	}
+}

+ 164 - 0
packages/ckeditor5-table/src/tablecellproperties/tablecellpropertiesediting.js

@@ -0,0 +1,164 @@
+/**
+ * @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/tablecellproperties/tablecellpropertiesediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import { addBorderRules } from '@ckeditor/ckeditor5-engine/src/view/styles/border';
+import { addPaddingRules } from '@ckeditor/ckeditor5-engine/src/view/styles/padding';
+import { addBackgroundRules } from '@ckeditor/ckeditor5-engine/src/view/styles/background';
+
+import { downcastAttributeToStyle, upcastStyleToAttribute, upcastBorderStyles } from './../converters/tableproperties';
+import TableEditing from './../tableediting';
+import TableCellPaddingCommand from './commands/tablecellpaddingcommand';
+import TableCellWidthCommand from './commands/tablecellwidthcommand';
+import TableCellHeightCommand from './commands/tablecellheightcommand';
+import TableCellBackgroundColorCommand from './commands/tablecellbackgroundcolorcommand';
+import TableCellVerticalAlignmentCommand from './commands/tablecellverticalalignmentcommand';
+import TableCellHorizontalAlignmentCommand from './commands/tablecellhorizontalalignmentcommand';
+import TableCellBorderStyleCommand from './commands/tablecellborderstylecommand';
+import TableCellBorderColorCommand from './commands/tablecellbordercolorcommand';
+import TableCellBorderWidthCommand from './commands/tablecellborderwidthcommand';
+
+/**
+ * The table cell properties editing feature.
+ *
+ * Introduces table cells's model attributes and their conversion:
+ *
+ * - border: `borderStyle`, `borderColor` and `borderWidth`
+ * - background color: `backgroundColor`
+ * - cell padding: `padding`
+ * - horizontal and vertical alignment: `horizontalAlignment`, `verticalAlignment`
+ * - cell width & height: `width` & `height`
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TableCellPropertiesEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableCellPropertiesEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ TableEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+		const conversion = editor.conversion;
+		const viewDoc = editor.editing.view.document;
+
+		viewDoc.addStyleProcessorRules( addBorderRules );
+		enableBorderProperties( schema, conversion );
+		editor.commands.add( 'tableCellBorderStyle', new TableCellBorderStyleCommand( editor ) );
+		editor.commands.add( 'tableCellBorderColor', new TableCellBorderColorCommand( editor ) );
+		editor.commands.add( 'tableCellBorderWidth', new TableCellBorderWidthCommand( editor ) );
+
+		enableHorizontalAlignmentProperty( schema, conversion );
+		editor.commands.add( 'tableCellHorizontalAlignment', new TableCellHorizontalAlignmentCommand( editor ) );
+
+		enableProperty( schema, conversion, 'width', 'width' );
+		editor.commands.add( 'tableCellWidth', new TableCellWidthCommand( editor ) );
+
+		enableProperty( schema, conversion, 'height', 'height' );
+		editor.commands.add( 'tableCellHeight', new TableCellHeightCommand( editor ) );
+
+		viewDoc.addStyleProcessorRules( addPaddingRules );
+		enableProperty( schema, conversion, 'padding', 'padding' );
+		editor.commands.add( 'tableCellPadding', new TableCellPaddingCommand( editor ) );
+
+		viewDoc.addStyleProcessorRules( addBackgroundRules );
+		enableProperty( schema, conversion, 'backgroundColor', 'background-color' );
+		editor.commands.add( 'tableCellBackgroundColor', new TableCellBackgroundColorCommand( editor ) );
+
+		enableProperty( schema, conversion, 'verticalAlignment', 'vertical-align' );
+		editor.commands.add( 'tableCellVerticalAlignment', new TableCellVerticalAlignmentCommand( editor ) );
+	}
+}
+
+// Enables `'borderStyle'`, `'borderColor'` and `'borderWidth'` attributes for table cells.
+//
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableBorderProperties( schema, conversion ) {
+	schema.extend( 'tableCell', {
+		allowAttributes: [ 'borderWidth', 'borderColor', 'borderStyle' ]
+	} );
+	upcastBorderStyles( conversion, 'td' );
+	upcastBorderStyles( conversion, 'th' );
+	downcastAttributeToStyle( conversion, 'tableCell', 'borderStyle', 'border-style' );
+	downcastAttributeToStyle( conversion, 'tableCell', 'borderColor', 'border-color' );
+	downcastAttributeToStyle( conversion, 'tableCell', 'borderWidth', 'border-width' );
+}
+
+// Enables `'horizontalAlignment'` attribute for table cells.
+//
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableHorizontalAlignmentProperty( schema, conversion ) {
+	schema.extend( 'tableCell', {
+		allowAttributes: [ 'horizontalAlignment' ]
+	} );
+
+	conversion.attributeToAttribute( {
+		model: {
+			name: 'tableCell',
+			key: 'horizontalAlignment',
+			values: [ 'left', 'right', 'center', 'justify' ]
+		},
+		view: {
+			// TODO: controversial one but I don't know if we want "default".
+			left: {
+				key: 'style',
+				value: {
+					'text-align': 'left'
+				}
+			},
+			right: {
+				key: 'style',
+				value: {
+					'text-align': 'right'
+				}
+			},
+			center: {
+				key: 'style',
+				value: {
+					'text-align': 'center'
+				}
+			},
+			justify: {
+				key: 'style',
+				value: {
+					'text-align': 'justify'
+				}
+			}
+		}
+	} );
+}
+
+// Enables conversion for an attribute for simple view-model mappings.
+//
+// @param {String} modelAttribute
+// @param {String} styleName
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableProperty( schema, conversion, modelAttribute, styleName ) {
+	schema.extend( 'tableCell', {
+		allowAttributes: [ modelAttribute ]
+	} );
+	upcastStyleToAttribute( conversion, 'tableCell', modelAttribute, styleName );
+	downcastAttributeToStyle( conversion, 'tableCell', modelAttribute, styleName );
+}

+ 0 - 46
packages/ckeditor5-table/src/tablecolumnrowproperties.js

@@ -1,46 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module table/tablecolumnrowproperties
- */
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import { downcastToStyle, upcastAttribute } from './tableproperties/utils';
-
-/**
- * The table column row properties feature.
- *
- * @extends module:core/plugin~Plugin
- */
-export default class TableColumnRowProperties extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'TableColumnRowProperties';
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	afterInit() {
-		const editor = this.editor;
-		const schema = editor.model.schema;
-		const conversion = editor.conversion;
-
-		schema.extend( 'tableCell', {
-			allowAttributes: [ 'height' ]
-		} );
-		upcastAttribute( conversion, 'tableCell', 'height', 'height' );
-		downcastToStyle( conversion, 'tableCell', 'height', 'height' );
-
-		schema.extend( 'tableCell', {
-			allowAttributes: [ 'width' ]
-		} );
-		upcastAttribute( conversion, 'tableCell', 'width', 'width' );
-		downcastToStyle( conversion, 'tableCell', 'width', 'width' );
-	}
-}

+ 5 - 118
packages/ckeditor5-table/src/tableproperties.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
@@ -8,7 +8,8 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import { downcastTableAttribute, upcastAttribute, upcastBorderStyles } from './tableproperties/utils';
+
+import TablePropertiesEditing from './tableproperties/tablepropertiesediting';
 
 /**
  * The table properties feature.
@@ -26,121 +27,7 @@ export default class TableProperties extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
-	afterInit() {
-		const editor = this.editor;
-		const schema = editor.model.schema;
-		const conversion = editor.conversion;
-
-		this.enableBorderProperties( schema, conversion );
-		this.enableBackgroundColorProperty( schema, conversion );
-		this.enableWidthProperty( schema, conversion );
-		this.enableHeightProperty( schema, conversion );
-		this.enableAlignmentProperty( schema, conversion );
-	}
-
-	enableBorderProperties( schema, conversion ) {
-		schema.extend( 'table', {
-			allowAttributes: [ 'borderWidth', 'borderColor', 'borderStyle' ]
-		} );
-		upcastBorderStyles( conversion, 'table' );
-		downcastTableAttribute( conversion, 'borderColor', 'border-color' );
-		downcastTableAttribute( conversion, 'borderStyle', 'border-style' );
-		downcastTableAttribute( conversion, 'borderWidth', 'border-width' );
-	}
-
-	enableBackgroundColorProperty( schema, conversion ) {
-		schema.extend( 'table', {
-			allowAttributes: [ 'backgroundColor' ]
-		} );
-		upcastAttribute( conversion, 'table', 'backgroundColor', 'background-color' );
-		downcastTableAttribute( conversion, 'backgroundColor', 'background-color' );
-	}
-
-	enableWidthProperty( schema, conversion ) {
-		schema.extend( 'table', {
-			allowAttributes: [ 'width' ]
-		} );
-		upcastAttribute( conversion, 'table', 'width', 'width' );
-		downcastTableAttribute( conversion, 'width', 'width' );
-	}
-
-	enableHeightProperty( schema, conversion ) {
-		schema.extend( 'table', {
-			allowAttributes: [ 'height' ]
-		} );
-		upcastAttribute( conversion, 'table', 'height', 'height' );
-		downcastTableAttribute( conversion, 'height', 'height' );
-	}
-
-	enableAlignmentProperty( schema, conversion ) {
-		schema.extend( 'table', {
-			allowAttributes: [ 'alignment' ]
-		} );
-		conversion.for( 'upcast' )
-			.attributeToAttribute( {
-				view: {
-					styles: {
-						'margin-right': /^(auto|0(%|[a-z]{2,4})?)$/,
-						'margin-left': /^(auto|0(%|[a-z]{2,4})?)$/
-					}
-				},
-				model: {
-					name: 'table',
-					key: 'alignment',
-					value: viewElement => {
-						// At this point we only have auto or 0 value (with a unit).
-						if ( viewElement.getStyle( 'margin-right' ) != 'auto' ) {
-							return 'right';
-						}
-
-						if ( viewElement.getStyle( 'margin-left' ) != 'auto' ) {
-							return 'left';
-						}
-
-						return 'center';
-					}
-				}
-			} )
-			// Support for backwards compatibility and pasting from other sources.
-			.attributeToAttribute( {
-				view: {
-					attributes: {
-						align: /^(left|right|center)$/
-					}
-				},
-				model: {
-					name: 'table',
-					key: 'alignment',
-					value: viewElement => viewElement.getAttribute( 'align' )
-				}
-			} );
-		conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( 'attribute:alignment:table', ( evt, data, conversionApi ) => {
-			const { item, attributeNewValue } = data;
-			const { mapper, writer } = conversionApi;
-
-			const table = [ ...mapper.toViewElement( item ).getChildren() ].find( child => child.is( 'table' ) );
-
-			if ( !attributeNewValue ) {
-				writer.removeStyle( 'margin-left', table );
-				writer.removeStyle( 'margin-right', table );
-
-				return;
-			}
-
-			const styles = {
-				'margin-right': 'auto',
-				'margin-left': 'auto'
-			};
-
-			if ( attributeNewValue == 'left' ) {
-				styles[ 'margin-left' ] = '0';
-			}
-
-			if ( attributeNewValue == 'right' ) {
-				styles[ 'margin-right' ] = '0';
-			}
-
-			writer.setStyle( styles, table );
-		} ) );
+	static get requires() {
+		return [ TablePropertiesEditing ];
 	}
 }

+ 35 - 0
packages/ckeditor5-table/src/tableproperties/commands/tablealignmentcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tableproperties/commands/tablealignmentcommand
+ */
+
+import TablePropertyCommand from './tablepropertycommand';
+
+/**
+ * The table alignment command.
+ *
+ * The command is registered by {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
+ * `'tableAlignment'` editor command.
+ *
+ * To change alignment of the selected table, execute the command:
+ *
+ *		editor.execute( 'tableAlignment', {
+ *			value: 'right'
+ *		} );
+ *
+ * @extends module:table/tableproperties/commands/tablepropertycommand
+ */
+export default class TableAlignmentCommand extends TablePropertyCommand {
+	/**
+	 * Creates a new `TableAlignmentCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'alignment' );
+	}
+}

+ 35 - 0
packages/ckeditor5-table/src/tableproperties/commands/tablebackgroundcolorcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tableproperties/commands/tablebackgroundcolorcommand
+ */
+
+import TablePropertyCommand from './tablepropertycommand';
+
+/**
+ * The table background color command.
+ *
+ * The command is registered by {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
+ * `'tableBackgroundColor'` editor command.
+ *
+ * To change backgroundColor of the selected, execute the command:
+ *
+ *		editor.execute( 'tableBackgroundColor', {
+ *			value: '#f00'
+ *		} );
+ *
+ * @extends module:table/tableproperties/commands/tablepropertycommand
+ */
+export default class TableBackgroundColorCommand extends TablePropertyCommand {
+	/**
+	 * Creates a new `TableBackgroundColorCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'backgroundColor' );
+	}
+}

+ 47 - 0
packages/ckeditor5-table/src/tableproperties/commands/tablebordercolorcommand.js

@@ -0,0 +1,47 @@
+/**
+ * @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/tableproperties/commands/tablebordercolorcommand
+ */
+
+import TablePropertyCommand from './tablepropertycommand';
+import { getSingleValue } from '../../commands/utils';
+
+/**
+ * The table border color command.
+ *
+ * The command is registered by {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
+ * `'tableBorderColor'` editor command.
+ *
+ * To change border color of the selected , execute the command:
+ *
+ *		editor.execute( 'tableBorderColor', {
+ *			value: '#f00'
+ *		} );
+ *
+ * @extends module:table/tableproperties/commands/tablepropertycommand
+ */
+export default class TableBorderColorCommand extends TablePropertyCommand {
+	/**
+	 * Creates a new `TableBorderColorCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'borderColor' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValue( table ) {
+		if ( !table ) {
+			return;
+		}
+
+		return getSingleValue( table.getAttribute( this.attributeName ) );
+	}
+}

+ 47 - 0
packages/ckeditor5-table/src/tableproperties/commands/tableborderstylecommand.js

@@ -0,0 +1,47 @@
+/**
+ * @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/tableproperties/commands/tableborderstylecommand
+ */
+
+import TablePropertyCommand from './tablepropertycommand';
+import { getSingleValue } from '../../commands/utils';
+
+/**
+ * The table style border command.
+ *
+ * The command is registered by {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
+ * `'tableBorderStyle'` editor command.
+ *
+ * To change border of the selected , execute the command:
+ *
+ *		editor.execute( 'tableBorderStyle', {
+ *			value: 'dashed'
+ *		} );
+ *
+ * @extends module:table/tableproperties/commands/tablepropertycommand
+ */
+export default class TableBorderStyleCommand extends TablePropertyCommand {
+	/**
+	 * Creates a new `TableBorderStyleCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'borderStyle' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValue( table ) {
+		if ( !table ) {
+			return;
+		}
+
+		return getSingleValue( table.getAttribute( this.attributeName ) );
+	}
+}

+ 47 - 0
packages/ckeditor5-table/src/tableproperties/commands/tableborderwidthcommand.js

@@ -0,0 +1,47 @@
+/**
+ * @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/tableproperties/commands/tableborderwidthcommand
+ */
+
+import TablePropertyCommand from './tablepropertycommand';
+import { getSingleValue } from '../../commands/utils';
+
+/**
+ * The table width border command.
+ *
+ * The command is registered by {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
+ * `'tableBorderWidth'` editor command.
+ *
+ * To change border of the selected , execute the command:
+ *
+ *		editor.execute( 'tableBorderWidth', {
+ *			value: '5px'
+ *		} );
+ *
+ * @extends module:table/tableproperties/commands/tablepropertycommand
+ */
+export default class TableBorderWidthCommand extends TablePropertyCommand {
+	/**
+	 * Creates a new `TableBorderWidthCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'borderWidth' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_getValue( table ) {
+		if ( !table ) {
+			return;
+		}
+
+		return getSingleValue( table.getAttribute( this.attributeName ) );
+	}
+}

+ 35 - 0
packages/ckeditor5-table/src/tableproperties/commands/tableheightcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tableproperties/commands/tableheightcommand
+ */
+
+import TablePropertyCommand from './tablepropertycommand';
+
+/**
+ * The table height command.
+ *
+ * The command is registered by {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
+ * `'tableHeight'` editor command.
+ *
+ * To change height of the selected table, execute the command:
+ *
+ *		editor.execute( 'tableHeight', {
+ *			value: '500px'
+ *		} );
+ *
+ * @extends module:core/command~Command
+ */
+export default class TableHeightCommand extends TablePropertyCommand {
+	/**
+	 * Creates a new `TableHeightCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'height' );
+	}
+}

+ 87 - 0
packages/ckeditor5-table/src/tableproperties/commands/tablepropertycommand.js

@@ -0,0 +1,87 @@
+/**
+ * @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/tableproperties/commands/tablepropertycommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+import { findAncestor } from '../../commands/utils';
+
+/**
+ * The table cell attribute command.
+ *
+ * The command is a base command for other table cell attributes commands.
+ *
+ * @extends module:core/command~Command
+ */
+export default class TablePropertyCommand extends Command {
+	/**
+	 * Creates a new `TablePropertyCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 * @param {String} attributeName Table cell attribute name.
+	 */
+	constructor( editor, attributeName ) {
+		super( editor );
+
+		this.attributeName = attributeName;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const editor = this.editor;
+		const selection = editor.model.document.selection;
+
+		const table = findAncestor( 'table', selection.getFirstPosition() );
+
+		this.isEnabled = !!table;
+		this.value = this._getValue( table );
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 * @fires execute
+	 * @param {Object} [options]
+	 * @param {*} [options.value] If set the command will set the attribute on selected table.
+	 * If it is not set the command will remove the attribute from selected table.
+	 * @param {module:engine/model/batch~Batch} [options.batch] Pass batch instance to the command for creating single undo step.
+	 */
+	execute( options = {} ) {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+
+		const { value, batch } = options;
+
+		const table = findAncestor( 'table', selection.getFirstPosition() );
+
+		model.enqueueChange( batch || 'default', writer => {
+			if ( value ) {
+				writer.setAttribute( this.attributeName, value, table );
+			} else {
+				writer.removeAttribute( this.attributeName, table );
+			}
+		} );
+	}
+
+	/**
+	 * Returns attribute value for a table cell.
+	 *
+	 * @param {module:engine/model/element~Element} table
+	 * @returns {String|undefined}
+	 * @private
+	 */
+	_getValue( table ) {
+		if ( !table ) {
+			return;
+		}
+
+		return table.getAttribute( this.attributeName );
+	}
+}

+ 35 - 0
packages/ckeditor5-table/src/tableproperties/commands/tablewidthcommand.js

@@ -0,0 +1,35 @@
+/**
+ * @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/tableproperties/commands/tablewidthcommand
+ */
+
+import TablePropertyCommand from './tablepropertycommand';
+
+/**
+ * The table width command.
+ *
+ * The command is registered by {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
+ * `'tableWidth'` editor command.
+ *
+ * To change width of the selected table, execute the command:
+ *
+ *		editor.execute( 'tableWidth', {
+ *			value: '400px'
+ *		} );
+ *
+ * @extends module:table/tableproperties/commands/tablepropertycommand
+ */
+export default class TableWidthCommand extends TablePropertyCommand {
+	/**
+	 * Creates a new `TableWidthCommand` instance.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+	 */
+	constructor( editor ) {
+		super( editor, 'width' );
+	}
+}

+ 176 - 0
packages/ckeditor5-table/src/tableproperties/tablepropertiesediting.js

@@ -0,0 +1,176 @@
+/**
+ * @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/tableproperties/tablepropertiesediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import { addBorderRules } from '@ckeditor/ckeditor5-engine/src/view/styles/border';
+import { addBackgroundRules } from '@ckeditor/ckeditor5-engine/src/view/styles/background';
+
+import TableEditing from './../tableediting';
+import { downcastTableAttribute, upcastStyleToAttribute, upcastBorderStyles } from './../converters/tableproperties';
+import TableBackgroundColorCommand from './commands/tablebackgroundcolorcommand';
+import TableBorderColorCommand from './commands/tablebordercolorcommand';
+import TableBorderStyleCommand from './commands/tableborderstylecommand';
+import TableBorderWidthCommand from './commands/tableborderwidthcommand';
+import TableWidthCommand from './commands/tablewidthcommand';
+import TableHeightCommand from './commands/tableheightcommand';
+import TableAlignmentCommand from './commands/tablealignmentcommand';
+
+/**
+ * The table properties editing feature.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TablePropertiesEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TablePropertiesEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ TableEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+		const conversion = editor.conversion;
+		const viewDoc = editor.editing.view.document;
+
+		viewDoc.addStyleProcessorRules( addBorderRules );
+		enableBorderProperties( schema, conversion );
+		editor.commands.add( 'tableBorderColor', new TableBorderColorCommand( editor ) );
+		editor.commands.add( 'tableBorderStyle', new TableBorderStyleCommand( editor ) );
+		editor.commands.add( 'tableBorderWidth', new TableBorderWidthCommand( editor ) );
+
+		enableAlignmentProperty( schema, conversion );
+		editor.commands.add( 'tableAlignment', new TableAlignmentCommand( editor ) );
+
+		enableProperty( schema, conversion, 'width', 'width' );
+		editor.commands.add( 'tableWidth', new TableWidthCommand( editor ) );
+
+		enableProperty( schema, conversion, 'height', 'height' );
+		editor.commands.add( 'tableHeight', new TableHeightCommand( editor ) );
+
+		viewDoc.addStyleProcessorRules( addBackgroundRules );
+		enableProperty( schema, conversion, 'backgroundColor', 'background-color' );
+		editor.commands.add( 'tableBackgroundColor', new TableBackgroundColorCommand( editor ) );
+	}
+}
+
+// Enables `'borderStyle'`, `'borderColor'` and `'borderWidth'` attributes for table.
+//
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableBorderProperties( schema, conversion ) {
+	schema.extend( 'table', {
+		allowAttributes: [ 'borderWidth', 'borderColor', 'borderStyle' ]
+	} );
+	upcastBorderStyles( conversion, 'table' );
+	downcastTableAttribute( conversion, 'borderColor', 'border-color' );
+	downcastTableAttribute( conversion, 'borderStyle', 'border-style' );
+	downcastTableAttribute( conversion, 'borderWidth', 'border-width' );
+}
+
+// Enables `'alignment'` attribute for table.
+//
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableAlignmentProperty( schema, conversion ) {
+	schema.extend( 'table', {
+		allowAttributes: [ 'alignment' ]
+	} );
+	conversion.for( 'upcast' )
+		.attributeToAttribute( {
+			view: {
+				styles: {
+					'margin-right': /^(auto|0(%|[a-z]{2,4})?)$/,
+					'margin-left': /^(auto|0(%|[a-z]{2,4})?)$/
+				}
+			},
+			model: {
+				name: 'table',
+				key: 'alignment',
+				value: viewElement => {
+					// At this point we only have auto or 0 value (with a unit).
+					if ( viewElement.getStyle( 'margin-right' ) != 'auto' ) {
+						return 'right';
+					}
+
+					if ( viewElement.getStyle( 'margin-left' ) != 'auto' ) {
+						return 'left';
+					}
+
+					return 'center';
+				}
+			}
+		} )
+		// Support for backwards compatibility and pasting from other sources.
+		.attributeToAttribute( {
+			view: {
+				attributes: {
+					align: /^(left|right|center)$/
+				}
+			},
+			model: {
+				name: 'table',
+				key: 'alignment',
+				value: viewElement => viewElement.getAttribute( 'align' )
+			}
+		} );
+	conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( 'attribute:alignment:table', ( evt, data, conversionApi ) => {
+		const { item, attributeNewValue } = data;
+		const { mapper, writer } = conversionApi;
+
+		const table = [ ...mapper.toViewElement( item ).getChildren() ].find( child => child.is( 'table' ) );
+
+		if ( !attributeNewValue ) {
+			writer.removeStyle( 'margin-left', table );
+			writer.removeStyle( 'margin-right', table );
+
+			return;
+		}
+
+		const styles = {
+			'margin-right': 'auto',
+			'margin-left': 'auto'
+		};
+
+		if ( attributeNewValue == 'left' ) {
+			styles[ 'margin-left' ] = '0';
+		}
+
+		if ( attributeNewValue == 'right' ) {
+			styles[ 'margin-right' ] = '0';
+		}
+
+		writer.setStyle( styles, table );
+	} ) );
+}
+
+// Enables conversion for an attribute for simple view-model mappings.
+//
+// @param {String} modelAttribute
+// @param {String} styleName
+// @param {module:engine/model/schema~Schema} schema
+// @param {module:engine/conversion/conversion~Conversion} conversion
+function enableProperty( schema, conversion, modelAttribute, styleName ) {
+	schema.extend( 'table', {
+		allowAttributes: [ modelAttribute ]
+	} );
+	upcastStyleToAttribute( conversion, 'table', modelAttribute, styleName );
+	downcastTableAttribute( conversion, modelAttribute, styleName );
+}

+ 67 - 1
packages/ckeditor5-table/tests/_utils/utils.js

@@ -13,6 +13,7 @@ import {
 } from '../../src/converters/downcast';
 import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable';
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 const WIDGET_TABLE_CELL_CLASS = 'ck-editor__editable ck-editor__nested-editable';
 
@@ -55,6 +56,69 @@ export function modelTable( tableData, attributes ) {
 	return `<table${ formatAttributes( attributes ) }>${ tableRows }</table>`;
 }
 
+/**
+ * Helper method for creating a test table with a single table cell which attributes might be objects.
+ *
+ *		setTableCellWithObjectAttributes(
+ *			model,
+ *			{
+ *				margin: { top: '1px', left: '2px' },
+ *				borderColor: { top: '#f00', left: '#ba2' }
+ *				backgroundColor: '#f00'
+ *			},
+ *			'fo[o]'
+ *		);
+ *
+ * This will create a model table with one table cell with a "foo" text. Selection will be set on last "o" and a table cell will have three
+ * attributes.
+ *
+ * @param {module:engine/model/model~Model} model
+ * @param {Object} attributes
+ * @param {String} cellContent
+ */
+export function setTableCellWithObjectAttributes( model, attributes, cellContent ) {
+	setData( model, modelTable( [ [ { contents: cellContent } ] ] ) );
+
+	const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+	model.change( writer => {
+		for ( const [ key, value ] of Object.entries( attributes ) ) {
+			writer.setAttribute( key, value, tableCell );
+		}
+	} );
+}
+
+/**
+ * Helper method for creating a test table, with a single table cell, which attributes might be objects.
+ *
+ *		setTableWithObjectAttributes(
+ *			model,
+ *			{
+ *				borderColor: { top: '#f00', left: '#ba2' }
+ *				backgroundColor: '#f00'
+ *			},
+ *			'fo[o]'
+ *		);
+ *
+ * This will create a model table with one table cell with a "foo" text. Selection will be set on last "o" and a table will have three
+ * attributes.
+ *
+ * @param {module:engine/model/model~Model} model
+ * @param {Object} attributes
+ * @param {String} cellContent
+ */
+export function setTableWithObjectAttributes( model, attributes, cellContent ) {
+	setData( model, modelTable( [ [ { contents: cellContent } ] ] ) );
+
+	const table = model.document.getRoot().getChild( 0 );
+
+	model.change( writer => {
+		for ( const [ key, value ] of Object.entries( attributes ) ) {
+			writer.setAttribute( key, value, table );
+		}
+	} );
+}
+
 /**
  * Returns a view representation of a table shorthand notation:
  *
@@ -250,7 +314,9 @@ export function assertTableStyle( editor, tableStyle ) {
  */
 export function assertTableCellStyle( editor, tableCellStyle ) {
 	assertEqualMarkup( editor.getData(),
-		`<figure class="table"><table><tbody><tr><td style="${ tableCellStyle }">foo</td></tr></tbody></table></figure>`
+		'<figure class="table"><table><tbody><tr>' +
+			`<td${ tableCellStyle ? ` style="${ tableCellStyle }"` : '' }>foo</td>` +
+		'</tr></tbody></table></figure>'
 	);
 }
 

+ 3 - 7
packages/ckeditor5-table/tests/manual/tableproperties.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
@@ -10,9 +10,9 @@ import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articleplugi
 import Alignment from '@ckeditor/ckeditor5-alignment/src/alignment';
 import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
 import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+
 import TableProperties from '../../src/tableproperties';
 import TableCellProperties from '../../src/tablecellproperties';
-import TableColumnRowProperties from '../../src/tablecolumnrowproperties';
 
 const sourceElement = document.querySelector( '#editor' );
 const clonedSource = sourceElement.cloneNode( true );
@@ -21,11 +21,7 @@ document.querySelector( '#cloned-source' ).append( ...clonedSource.childNodes );
 
 ClassicEditor
 	.create( sourceElement, {
-		plugins: [
-			ArticlePluginSet, Alignment, Indent, IndentBlock,
-
-			TableProperties, TableColumnRowProperties, TableCellProperties
-		],
+		plugins: [ ArticlePluginSet, Alignment, Indent, IndentBlock, TableProperties, TableCellProperties ],
 		toolbar: [
 			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
 		],

+ 23 - 670
packages/ckeditor5-table/tests/tablecellproperties.js

@@ -1,696 +1,49 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* global document */
-
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import TableEditing from '../src/tableediting';
 import TableCellProperties from '../src/tablecellproperties';
-import TableCellPropertiesUI from '../src/tablecellpropertiesui';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
-import { assertTableCellStyle, assertTRBLAttribute } from './_utils/utils';
+import TableCellPropertiesEditing from '../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellPropertiesUI from '../src/tablecellproperties/tablecellpropertiesui';
 
-describe( 'TableCellProperties', () => {
-	let element, editor, model;
+describe( 'table cell properties', () => {
+	let editor, editorElement;
 
-	beforeEach( () => {
-		element = document.createElement( 'div' );
+	describe( 'TableCellProperties', () => {
+		beforeEach( async () => {
+			editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
 
-		document.body.appendChild( element );
-
-		return ClassicTestEditor
-			.create( element, {
+			editor = await ClassicTestEditor.create( editorElement, {
 				plugins: [ TableCellProperties, Paragraph, TableEditing ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-
-				model = editor.model;
-			} );
-	} );
-
-	afterEach( () => {
-		editor.destroy();
-		element.remove();
-	} );
-
-	it( 'should have pluginName', () => {
-		expect( TableCellProperties.pluginName ).to.equal( 'TableCellProperties' );
-	} );
-
-	it( 'should require TableCellPropertiesUI', () => {
-		expect( TableCellProperties.requires ).to.deep.equal( [ TableCellPropertiesUI ] );
-	} );
-
-	describe( 'border', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'borderColor' ) ).to.be.true;
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'borderStyle' ) ).to.be.true;
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'borderWidth' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast border shorthand', () => {
-				editor.setData( '<table><tr><td style="border:1px solid #f00">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', '#f00' );
-				assertTRBLAttribute( tableCell, 'borderStyle', 'solid' );
-				assertTRBLAttribute( tableCell, 'borderWidth', '1px' );
-			} );
-
-			it( 'should upcast border-color shorthand', () => {
-				editor.setData( '<table><tr><td style="border-color:#f00">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', '#f00' );
-			} );
-
-			it( 'should upcast border-style shorthand', () => {
-				editor.setData( '<table><tr><td style="border-style:ridge">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderStyle', 'ridge' );
-			} );
-
-			it( 'should upcast border-width shorthand', () => {
-				editor.setData( '<table><tr><td style="border-width:1px">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderWidth', '1px' );
-			} );
-
-			it( 'should upcast border-top shorthand', () => {
-				editor.setData( '<table><tr><td style="border-top:1px solid #f00">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', '#f00', null, null, null );
-				assertTRBLAttribute( tableCell, 'borderStyle', 'solid', null, null, null );
-				assertTRBLAttribute( tableCell, 'borderWidth', '1px', null, null, null );
-			} );
-
-			it( 'should upcast border-right shorthand', () => {
-				editor.setData( '<table><tr><td style="border-right:1px solid #f00">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', null, '#f00', null, null );
-				assertTRBLAttribute( tableCell, 'borderStyle', null, 'solid', null, null );
-				assertTRBLAttribute( tableCell, 'borderWidth', null, '1px', null, null );
-			} );
-
-			it( 'should upcast border-bottom shorthand', () => {
-				editor.setData( '<table><tr><td style="border-bottom:1px solid #f00">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', null, null, '#f00', null );
-				assertTRBLAttribute( tableCell, 'borderStyle', null, null, 'solid', null );
-				assertTRBLAttribute( tableCell, 'borderWidth', null, null, '1px', null );
-			} );
-
-			it( 'should upcast border-left shorthand', () => {
-				editor.setData( '<table><tr><td style="border-left:1px solid #f00">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', null, null, null, '#f00' );
-				assertTRBLAttribute( tableCell, 'borderStyle', null, null, null, 'solid' );
-				assertTRBLAttribute( tableCell, 'borderWidth', null, null, null, '1px' );
-			} );
-
-			it( 'should upcast mixed shorthands', () => {
-				editor.setData(
-					'<table><tr><td style="border-top:1px solid #f00;border-bottom:2em ridge rgba(255,0,0,1)">foo</td></tr></table>'
-				);
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', '#f00', null, 'rgba(255, 0, 0, 1)', null );
-				assertTRBLAttribute( tableCell, 'borderStyle', 'solid', null, 'ridge', null );
-				assertTRBLAttribute( tableCell, 'borderWidth', '1px', null, '2em', null );
-			} );
-
-			it( 'should upcast border-top-* styles', () => {
-				editor.setData(
-					'<table><tr><td style="border-top-width:1px;border-top-style:solid;border-top-color:#f00">foo</td></tr></table>'
-				);
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', '#f00', null, null, null );
-				assertTRBLAttribute( tableCell, 'borderStyle', 'solid', null, null, null );
-				assertTRBLAttribute( tableCell, 'borderWidth', '1px', null, null, null );
-			} );
-
-			it( 'should upcast border-right-* styles', () => {
-				editor.setData(
-					'<table><tr><td style="border-right-width:1px;border-right-style:solid;border-right-color:#f00">foo</td></tr></table>'
-				);
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', null, '#f00', null, null );
-				assertTRBLAttribute( tableCell, 'borderStyle', null, 'solid', null, null );
-				assertTRBLAttribute( tableCell, 'borderWidth', null, '1px', null, null );
-			} );
-
-			it( 'should upcast border-bottom-* styles', () => {
-				editor.setData(
-					'<table><tr>' +
-					'<td style="border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#f00">foo</td>' +
-					'</tr></table>'
-				);
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', null, null, '#f00', null );
-				assertTRBLAttribute( tableCell, 'borderStyle', null, null, 'solid', null );
-				assertTRBLAttribute( tableCell, 'borderWidth', null, null, '1px', null );
-			} );
-
-			it( 'should upcast border-left-* styles', () => {
-				editor.setData(
-					'<table><tr><td style="border-left-width:1px;border-left-style:solid;border-left-color:#f00">foo</td></tr></table>'
-				);
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'borderColor', null, null, null, '#f00' );
-				assertTRBLAttribute( tableCell, 'borderStyle', null, null, null, 'solid' );
-				assertTRBLAttribute( tableCell, 'borderWidth', null, null, null, '1px' );
-			} );
-
-			it( 'should allow to be overriden (only border-top consumed)', () => {
-				editor.conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:td', ( evt, data, conversionApi ) => {
-					conversionApi.consumable.consume( data.viewItem, {
-						styles: [ 'border-top' ]
-					} );
-				}, { priority: 'high' } ) );
-
-				editor.setData( '<table><tr><td style="border:1px solid blue;">foo</td></tr></table>' );
-
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'borderColor' ) ).to.be.undefined;
-				expect( tableCell.getAttribute( 'borderStyle' ) ).to.be.undefined;
-				expect( tableCell.getAttribute( 'borderWidth' ) ).to.be.undefined;
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let tableCell;
-
-			beforeEach( () => {
-				setModelData(
-					model,
-					'<table headingRows="0" headingColumns="0">' +
-						'<tableRow>' +
-							'<tableCell>' +
-								'<paragraph>foo</paragraph>' +
-							'</tableCell>' +
-						'</tableRow>' +
-					'</table>'
-				);
-				tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-			} );
-
-			it( 'should downcast borderColor attribute (same top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderColor', {
-					top: '#f00',
-					right: '#f00',
-					bottom: '#f00',
-					left: '#f00'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor, 'border-top:#f00;border-right:#f00;border-bottom:#f00;border-left:#f00;' );
-			} );
-
-			it( 'should downcast borderColor attribute (different top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderColor', {
-					top: '#f00',
-					right: 'hsla(0, 100%, 50%, 0.5)',
-					bottom: 'deeppink',
-					left: 'rgb(255, 0, 0)'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor,
-					'border-top:#f00;' +
-					'border-right:hsla(0, 100%, 50%, 0.5);' +
-					'border-bottom:deeppink;' +
-					'border-left:rgb(255, 0, 0);'
-				);
-			} );
-
-			it( 'should downcast borderStyle attribute (same top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderStyle', {
-					top: 'solid',
-					right: 'solid',
-					bottom: 'solid',
-					left: 'solid'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor, 'border-top:solid;border-right:solid;border-bottom:solid;border-left:solid;' );
-			} );
-
-			it( 'should downcast borderStyle attribute (different top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderStyle', {
-					top: 'solid',
-					right: 'ridge',
-					bottom: 'dotted',
-					left: 'dashed'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor, 'border-top:solid;border-right:ridge;border-bottom:dotted;border-left:dashed;' );
-			} );
-
-			it( 'should downcast borderWidth attribute (same top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderWidth', {
-					top: '42px',
-					right: '.1em',
-					bottom: '1337rem',
-					left: 'thick'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor, 'border-top:42px;border-right:.1em;border-bottom:1337rem;border-left:thick;' );
-			} );
-
-			it( 'should downcast borderWidth attribute (different top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderWidth', {
-					top: '42px',
-					right: '42px',
-					bottom: '42px',
-					left: '42px'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor, 'border-top:42px;border-right:42px;border-bottom:42px;border-left:42px;' );
-			} );
-
-			it( 'should downcast borderColor, borderStyle and borderWidth attributes together (same top, right, bottom, left)', () => {
-				model.change( writer => {
-					writer.setAttribute( 'borderColor', {
-						top: '#f00',
-						right: '#f00',
-						bottom: '#f00',
-						left: '#f00'
-					}, tableCell );
-
-					writer.setAttribute( 'borderStyle', {
-						top: 'solid',
-						right: 'solid',
-						bottom: 'solid',
-						left: 'solid'
-					}, tableCell );
-
-					writer.setAttribute( 'borderWidth', {
-						top: '42px',
-						right: '42px',
-						bottom: '42px',
-						left: '42px'
-					}, tableCell );
-				} );
-
-				assertTableCellStyle( editor,
-					'border-top:42px solid #f00;' +
-					'border-right:42px solid #f00;' +
-					'border-bottom:42px solid #f00;' +
-					'border-left:42px solid #f00;'
-				);
-			} );
-
-			it( 'should downcast borderColor, borderStyle and borderWidth attributes together (different top, right, bottom, left)', () => {
-				model.change( writer => {
-					writer.setAttribute( 'borderColor', {
-						top: '#f00',
-						right: 'hsla(0, 100%, 50%, 0.5)',
-						bottom: 'deeppink',
-						left: 'rgb(255, 0, 0)'
-					}, tableCell );
-
-					writer.setAttribute( 'borderStyle', {
-						top: 'solid',
-						right: 'ridge',
-						bottom: 'dotted',
-						left: 'dashed'
-					}, tableCell );
-
-					writer.setAttribute( 'borderWidth', {
-						top: '42px',
-						right: '.1em',
-						bottom: '1337rem',
-						left: 'thick'
-					}, tableCell );
-				} );
-
-				assertTableCellStyle( editor,
-					'border-top:42px solid #f00;' +
-					'border-right:.1em ridge hsla(0, 100%, 50%, 0.5);' +
-					'border-bottom:1337rem dotted deeppink;' +
-					'border-left:thick dashed rgb(255, 0, 0);'
-				);
-			} );
-
-			describe( 'change attribute', () => {
-				beforeEach( () => {
-					model.change( writer => {
-						writer.setAttribute( 'borderColor', {
-							top: '#f00',
-							right: '#f00',
-							bottom: '#f00',
-							left: '#f00'
-						}, tableCell );
-
-						writer.setAttribute( 'borderStyle', {
-							top: 'solid',
-							right: 'solid',
-							bottom: 'solid',
-							left: 'solid'
-						}, tableCell );
-
-						writer.setAttribute( 'borderWidth', {
-							top: '42px',
-							right: '42px',
-							bottom: '42px',
-							left: '42px'
-						}, tableCell );
-					} );
-				} );
-
-				it( 'should downcast borderColor attribute change', () => {
-					model.change( writer => writer.setAttribute( 'borderColor', {
-						top: 'deeppink',
-						right: 'deeppink',
-						bottom: 'deeppink',
-						left: 'deeppink'
-					}, tableCell ) );
-
-					assertTableCellStyle( editor,
-						'border-top:42px solid deeppink;' +
-						'border-right:42px solid deeppink;' +
-						'border-bottom:42px solid deeppink;' +
-						'border-left:42px solid deeppink;'
-					);
-				} );
-
-				it( 'should downcast borderStyle attribute change', () => {
-					model.change( writer => writer.setAttribute( 'borderStyle', {
-						top: 'ridge',
-						right: 'ridge',
-						bottom: 'ridge',
-						left: 'ridge'
-					}, tableCell ) );
-
-					assertTableCellStyle( editor,
-						'border-top:42px ridge #f00;' +
-						'border-right:42px ridge #f00;' +
-						'border-bottom:42px ridge #f00;' +
-						'border-left:42px ridge #f00;'
-					);
-				} );
-
-				it( 'should downcast borderWidth attribute change', () => {
-					model.change( writer => writer.setAttribute( 'borderWidth', {
-						top: 'thick',
-						right: 'thick',
-						bottom: 'thick',
-						left: 'thick'
-					}, tableCell ) );
-
-					assertTableCellStyle( editor,
-						'border-top:thick solid #f00;' +
-						'border-right:thick solid #f00;' +
-						'border-bottom:thick solid #f00;' +
-						'border-left:thick solid #f00;'
-					);
-				} );
-
-				it( 'should downcast borderColor attribute removal', () => {
-					model.change( writer => writer.removeAttribute( 'borderColor', tableCell ) );
-
-					assertTableCellStyle( editor,
-						'border-top:42px solid;' +
-						'border-right:42px solid;' +
-						'border-bottom:42px solid;' +
-						'border-left:42px solid;'
-					);
-				} );
-
-				it( 'should downcast borderStyle attribute removal', () => {
-					model.change( writer => writer.removeAttribute( 'borderStyle', tableCell ) );
-
-					assertTableCellStyle( editor,
-						'border-top:42px #f00;' +
-						'border-right:42px #f00;' +
-						'border-bottom:42px #f00;' +
-						'border-left:42px #f00;'
-					);
-				} );
-
-				it( 'should downcast borderWidth attribute removal', () => {
-					model.change( writer => writer.removeAttribute( 'borderWidth', tableCell ) );
-
-					assertTableCellStyle( editor,
-						'border-top:solid #f00;' +
-						'border-right:solid #f00;' +
-						'border-bottom:solid #f00;' +
-						'border-left:solid #f00;'
-					);
-				} );
-
-				it( 'should downcast borderColor, borderStyle and borderWidth attributes removal', () => {
-					model.change( writer => {
-						writer.removeAttribute( 'borderColor', tableCell );
-						writer.removeAttribute( 'borderStyle', tableCell );
-						writer.removeAttribute( 'borderWidth', tableCell );
-					} );
-
-					assertEqualMarkup(
-						editor.getData(),
-						'<figure class="table"><table><tbody><tr><td>foo</td></tr></tbody></table></figure>'
-					);
-				} );
-			} );
-		} );
-	} );
-
-	describe( 'background color', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'backgroundColor' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast background-color', () => {
-				editor.setData( '<table><tr><td style="background-color:#f00">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let tableCell;
-
-			beforeEach( () => {
-				setModelData(
-					model,
-					'<table headingRows="0" headingColumns="0">' +
-					'<tableRow>' +
-					'<tableCell>' +
-					'<paragraph>foo</paragraph>' +
-					'</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-				tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-			} );
-
-			it( 'should downcast backgroundColor', () => {
-				model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', tableCell ) );
-
-				assertTableCellStyle( editor, 'background-color:#f00;' );
-			} );
-		} );
-	} );
-
-	describe( 'horizontal alignment', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'horizontalAlignment' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast text-align:left style', () => {
-				editor.setData( '<table><tr><td style="text-align:left">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'left' );
-			} );
-
-			it( 'should upcast text-align:right style', () => {
-				editor.setData( '<table><tr><td style="text-align:right">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'right' );
-			} );
-
-			it( 'should upcast text-align:center style', () => {
-				editor.setData( '<table><tr><td style="text-align:center">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'center' );
-			} );
-
-			it( 'should upcast text-align:justify style', () => {
-				editor.setData( '<table><tr><td style="text-align:justify">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'justify' );
 			} );
 		} );
 
-		describe( 'downcast conversion', () => {
-			let tableCell;
-
-			beforeEach( () => {
-				setModelData(
-					model,
-					'<table headingRows="0" headingColumns="0">' +
-					'<tableRow>' +
-					'<tableCell>' +
-					'<paragraph>foo</paragraph>' +
-					'</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-				tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-			} );
-
-			it( 'should downcast horizontalAlignment=left', () => {
-				model.change( writer => writer.setAttribute( 'horizontalAlignment', 'left', tableCell ) );
-
-				assertTableCellStyle( editor, 'text-align:left;' );
-			} );
-
-			it( 'should downcast horizontalAlignment=right', () => {
-				model.change( writer => writer.setAttribute( 'horizontalAlignment', 'right', tableCell ) );
-
-				assertTableCellStyle( editor, 'text-align:right;' );
-			} );
-
-			it( 'should downcast horizontalAlignment=center', () => {
-				model.change( writer => writer.setAttribute( 'horizontalAlignment', 'center', tableCell ) );
-
-				assertTableCellStyle( editor, 'text-align:center;' );
-			} );
-
-			it( 'should downcast horizontalAlignment=justify', () => {
-				model.change( writer => writer.setAttribute( 'horizontalAlignment', 'justify', tableCell ) );
-
-				assertTableCellStyle( editor, 'text-align:justify;' );
-			} );
-		} );
-	} );
-
-	describe( 'vertical alignment', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'verticalAlignment' ) ).to.be.true;
+		afterEach( async () => {
+			editorElement.remove();
+			await editor.destroy();
 		} );
 
-		describe( 'upcast conversion', () => {
-			it( 'should upcast vertical-align', () => {
-				editor.setData( '<table><tr><td style="vertical-align:top">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.equal( 'top' );
-			} );
+		it( 'should be loaded', () => {
+			expect( editor.plugins.get( TableCellProperties ) ).to.instanceOf( TableCellProperties );
 		} );
 
-		describe( 'downcast conversion', () => {
-			let tableCell;
-
-			beforeEach( () => {
-				setModelData(
-					model,
-					'<table headingRows="0" headingColumns="0">' +
-					'<tableRow>' +
-					'<tableCell>' +
-					'<paragraph>foo</paragraph>' +
-					'</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-				tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-			} );
-
-			it( 'should downcast verticalAlignment', () => {
-				model.change( writer => writer.setAttribute( 'verticalAlignment', 'middle', tableCell ) );
-
-				assertTableCellStyle( editor, 'vertical-align:middle;' );
-			} );
+		it( 'should load TableCellPropertiesUI plugin', () => {
+			expect( editor.plugins.get( TableCellPropertiesUI ) ).to.instanceOf( TableCellPropertiesUI );
 		} );
-	} );
 
-	describe( 'padding', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'padding' ) ).to.be.true;
+		it( 'should load TableCellPropertiesEditing plugin', () => {
+			expect( editor.plugins.get( TableCellPropertiesEditing ) ).to.instanceOf( TableCellPropertiesEditing );
 		} );
 
-		describe( 'upcast conversion', () => {
-			it( 'should upcast padding shorthand', () => {
-				editor.setData( '<table><tr><td style="padding:2px 4em">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				assertTRBLAttribute( tableCell, 'padding', '2px', '4em' );
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let tableCell;
-
-			beforeEach( () => {
-				setModelData(
-					model,
-					'<table headingRows="0" headingColumns="0">' +
-					'<tableRow>' +
-					'<tableCell>' +
-					'<paragraph>foo</paragraph>' +
-					'</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-				tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-			} );
-
-			it( 'should downcast padding (same top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'padding', {
-					top: '2px',
-					right: '2px',
-					bottom: '2px',
-					left: '2px'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor, 'padding:2px;' );
-			} );
-
-			it( 'should downcast padding (different top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'padding', {
-					top: '2px',
-					right: '3px',
-					bottom: '4px',
-					left: '5px'
-				}, tableCell ) );
-
-				assertTableCellStyle( editor, 'padding:2px 3px 4px 5px;' );
-			} );
+		it( 'should have pluginName', () => {
+			expect( TableCellProperties.pluginName ).to.equal( 'TableCellProperties' );
 		} );
 	} );
 } );

+ 153 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbackgroundcolorcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellBackgroundColorCommand from '../../../src/tablecellproperties/commands/tablecellbackgroundcolorcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellBackgroundColorCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellBackgroundColorCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no backgroundColor property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has backgroundColor property', () => {
+						setData( model, modelTable( [ [ { backgroundColor: 'blue', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { backgroundColor: 'blue', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '#f00', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should change selected table cell backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ { backgroundColor: 'blue', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should remove backgroundColor from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { backgroundColor: 'blue', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should change selected table cell backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should remove backgroundColor from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 178 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellbordercolorcommand.js

@@ -0,0 +1,178 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellBorderColorCommand from '../../../src/tablecellproperties/commands/tablecellbordercolorcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellBorderColorCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellBorderColorCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no borderColor property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has borderColor property (single string)', () => {
+						setData( model, modelTable( [ [ { borderColor: 'blue', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+
+					it( 'should be set if selected table cell has borderColor property object with same values', () => {
+						setTableCellWithObjectAttributes( model, {
+							borderColor: {
+								top: 'blue',
+								right: 'blue',
+								bottom: 'blue',
+								left: 'blue'
+							}
+						}, '[]foo' );
+						expect( command.value ).to.equal( 'blue' );
+					} );
+
+					it( 'should be undefined if selected table cell has borderColor property object with different values', () => {
+						setTableCellWithObjectAttributes( model, {
+							borderColor: {
+								top: 'blue',
+								right: 'red',
+								bottom: 'blue',
+								left: 'blue'
+							}
+						}, '[]foo' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { borderColor: 'blue', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '#f00', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should change selected table cell borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ { borderColor: 'blue', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should remove borderColor from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { borderColor: 'blue', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should change selected table cell borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableCellStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should remove borderColor from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 178 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderstylecommand.js

@@ -0,0 +1,178 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellBorderStyleCommand from '../../../src/tablecellproperties/commands/tablecellborderstylecommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellBorderStyleCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellBorderStyleCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no borderStyle property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has borderStyle property (single string)', () => {
+						setData( model, modelTable( [ [ { borderStyle: 'ridge', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( 'ridge' );
+					} );
+
+					it( 'should be set if selected table cell has borderStyle property object with same values', () => {
+						setTableCellWithObjectAttributes( model, {
+							borderStyle: {
+								top: 'ridge',
+								right: 'ridge',
+								bottom: 'ridge',
+								left: 'ridge'
+							}
+						}, '[]foo' );
+						expect( command.value ).to.equal( 'ridge' );
+					} );
+
+					it( 'should be undefined if selected table cell has borderStyle property object with different values', () => {
+						setTableCellWithObjectAttributes( model, {
+							borderStyle: {
+								top: 'ridge',
+								right: 'dashed',
+								bottom: 'ridge',
+								left: 'ridge'
+							}
+						}, '[]foo' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { borderStyle: 'ridge', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( 'ridge' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: 'solid', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableCellStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should change selected table cell borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ { borderStyle: 'ridge', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableCellStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should remove borderStyle from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { borderStyle: 'ridge', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableCellStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should change selected table cell borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableCellStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should remove borderStyle from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 178 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellborderwidthcommand.js

@@ -0,0 +1,178 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellBorderWidthCommand from '../../../src/tablecellproperties/commands/tablecellborderwidthcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellBorderWidthCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellBorderWidthCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no borderWidth property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has borderWidth property (single string)', () => {
+						setData( model, modelTable( [ [ { borderWidth: '2em', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( '2em' );
+					} );
+
+					it( 'should be set if selected table cell has borderWidth property object with same values', () => {
+						setTableCellWithObjectAttributes( model, {
+							borderWidth: {
+								top: '2em',
+								right: '2em',
+								bottom: '2em',
+								left: '2em'
+							}
+						}, '[]foo' );
+						expect( command.value ).to.equal( '2em' );
+					} );
+
+					it( 'should be undefined if selected table cell has borderWidth property object with different values', () => {
+						setTableCellWithObjectAttributes( model, {
+							borderWidth: {
+								top: '2em',
+								right: '333px',
+								bottom: '2em',
+								left: '2em'
+							}
+						}, '[]foo' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { borderWidth: '2em', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( '2em' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '1px', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableCellStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should change selected table cell borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ { borderWidth: '2em', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableCellStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should remove borderWidth from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { borderWidth: '2em', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableCellStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should change selected table cell borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableCellStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should remove borderWidth from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellheightcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellHeightCommand from '../../../src/tablecellproperties/commands/tablecellheightcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellHeightCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellHeightCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no height property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has height property', () => {
+						setData( model, modelTable( [ [ { height: '100px', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { height: '100px', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '25px', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell height to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should change selected table cell height to a passed value', () => {
+						setData( model, modelTable( [ [ { height: '100px', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should remove height from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { height: '100px', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell height to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should change selected table cell height to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should remove height from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellhorizontalalignmentcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellHorizontalAlignmentCommand from '../../../src/tablecellproperties/commands/tablecellhorizontalalignmentcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellHorizontalAlignmentCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellHorizontalAlignmentCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no horizontalAlignment property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has horizontalAlignment property', () => {
+						setData( model, modelTable( [ [ { horizontalAlignment: 'center', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( 'center' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { horizontalAlignment: 'center', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( 'center' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: 'right', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell horizontalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableCellStyle( editor, 'text-align:right;' );
+					} );
+
+					it( 'should change selected table cell horizontalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ { horizontalAlignment: 'center', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableCellStyle( editor, 'text-align:right;' );
+					} );
+
+					it( 'should remove horizontalAlignment from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { horizontalAlignment: 'center', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell horizontalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableCellStyle( editor, 'text-align:right;' );
+					} );
+
+					it( 'should change selected table cell horizontalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableCellStyle( editor, 'text-align:right;' );
+					} );
+
+					it( 'should remove horizontalAlignment from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 178 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellpaddingcommand.js

@@ -0,0 +1,178 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable, setTableCellWithObjectAttributes } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellPaddingCommand from '../../../src/tablecellproperties/commands/tablecellpaddingcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellPaddingCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellPaddingCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no padding property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has padding property (single string)', () => {
+						setData( model, modelTable( [ [ { padding: '2em', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( '2em' );
+					} );
+
+					it( 'should be set if selected table cell has padding property object with same values', () => {
+						setTableCellWithObjectAttributes( model, {
+							padding: {
+								top: '2em',
+								right: '2em',
+								bottom: '2em',
+								left: '2em'
+							}
+						}, '[]foo' );
+						expect( command.value ).to.equal( '2em' );
+					} );
+
+					it( 'should be undefined if selected table cell has padding property object with different values', () => {
+						setTableCellWithObjectAttributes( model, {
+							padding: {
+								top: '2em',
+								right: '1px',
+								bottom: '2em',
+								left: '2em'
+							}
+						}, '[]foo' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { padding: '2em', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( '2em' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '0.5em', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell padding to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '0.5em' } );
+
+						assertTableCellStyle( editor, 'padding:0.5em;' );
+					} );
+
+					it( 'should change selected table cell padding to a passed value', () => {
+						setData( model, modelTable( [ [ { padding: '2em', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: '0.5em' } );
+
+						assertTableCellStyle( editor, 'padding:0.5em;' );
+					} );
+
+					it( 'should remove padding from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { padding: '2em', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell padding to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '0.5em' } );
+
+						assertTableCellStyle( editor, 'padding:0.5em;' );
+					} );
+
+					it( 'should change selected table cell padding to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '0.5em' } );
+
+						assertTableCellStyle( editor, 'padding:0.5em;' );
+					} );
+
+					it( 'should remove padding from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellverticalalignmentcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellVerticalAlignmentCommand from '../../../src/tablecellproperties/commands/tablecellverticalalignmentcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellVerticalAlignmentCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellVerticalAlignmentCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no verticalAlignment property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has verticalAlignment property', () => {
+						setData( model, modelTable( [ [ { verticalAlignment: 'bottom', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( 'bottom' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { verticalAlignment: 'bottom', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( 'bottom' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: 'top', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell verticalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: 'top' } );
+
+						assertTableCellStyle( editor, 'vertical-align:top;' );
+					} );
+
+					it( 'should change selected table cell verticalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ { verticalAlignment: 'bottom', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: 'top' } );
+
+						assertTableCellStyle( editor, 'vertical-align:top;' );
+					} );
+
+					it( 'should remove verticalAlignment from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { verticalAlignment: 'bottom', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell verticalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'top' } );
+
+						assertTableCellStyle( editor, 'vertical-align:top;' );
+					} );
+
+					it( 'should change selected table cell verticalAlignment to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'top' } );
+
+						assertTableCellStyle( editor, 'vertical-align:top;' );
+					} );
+
+					it( 'should remove verticalAlignment from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-table/tests/tablecellproperties/commands/tablecellwidthcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableCellStyle, modelTable } from '../../_utils/utils';
+import TableCellPropertiesEditing from '../../../src/tablecellproperties/tablecellpropertiesediting';
+import TableCellWidthCommand from '../../../src/tablecellproperties/commands/tablecellwidthcommand';
+
+describe( 'table cell properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableCellWidthCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TableCellPropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableCellWidthCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table cell has no width property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table cell has width property', () => {
+						setData( model, modelTable( [ [ { width: '100px', contents: '[]foo' } ] ] ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table cell', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table cell', () => {
+						setData( model, modelTable( [ [ { width: '100px', contents: 'f[o]o' } ] ] ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '25px', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table cell width to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should change selected table cell width to a passed value', () => {
+						setData( model, modelTable( [ [ { width: '100px', contents: '[]foo' } ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should remove width from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ { width: '100px', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table cell width to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should change selected table cell width to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableCellStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should remove width from a selected table cell if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableCellStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 828 - 0
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js

@@ -0,0 +1,828 @@
+/**
+ * @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 Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import TableEditing from '../../src/tableediting';
+import TableCellPropertiesEditing from '../../src/tablecellproperties/tablecellpropertiesediting';
+
+import TableCellBorderColorCommand from '../../src/tablecellproperties/commands/tablecellbordercolorcommand';
+import TableCellBorderStyleCommand from '../../src/tablecellproperties/commands/tablecellborderstylecommand';
+import TableCellBorderWidthCommand from '../../src/tablecellproperties/commands/tablecellborderwidthcommand';
+import TableCellHorizontalAlignmentCommand from '../../src/tablecellproperties/commands/tablecellhorizontalalignmentcommand';
+import TableCellWidthCommand from '../../src/tablecellproperties/commands/tablecellwidthcommand';
+import TableCellHeightCommand from '../../src/tablecellproperties/commands/tablecellheightcommand';
+import TableCellVerticalAlignmentCommand from '../../src/tablecellproperties/commands/tablecellverticalalignmentcommand';
+import TableCellPaddingCommand from '../../src/tablecellproperties/commands/tablecellpaddingcommand';
+import TableCellBackgroundColorCommand from '../../src/tablecellproperties/commands/tablecellbackgroundcolorcommand';
+
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { assertTableCellStyle, assertTRBLAttribute } from '../_utils/utils';
+
+describe( 'table cell properties', () => {
+	describe( 'TableCellPropertiesEditing', () => {
+		let editor, model;
+
+		beforeEach( async () => {
+			editor = await VirtualTestEditor.create( {
+				plugins: [ TableCellPropertiesEditing, Paragraph, TableEditing ]
+			} );
+
+			model = editor.model;
+		} );
+
+		afterEach( async () => {
+			await editor.destroy();
+		} );
+
+		it( 'should have pluginName', () => {
+			expect( TableCellPropertiesEditing.pluginName ).to.equal( 'TableCellPropertiesEditing' );
+		} );
+
+		it( 'adds tableCellBorderColor command', () => {
+			expect( editor.commands.get( 'tableCellBorderColor' ) ).to.be.instanceOf( TableCellBorderColorCommand );
+		} );
+
+		it( 'adds tableCellBorderStyle command', () => {
+			expect( editor.commands.get( 'tableCellBorderStyle' ) ).to.be.instanceOf( TableCellBorderStyleCommand );
+		} );
+
+		it( 'adds tableCellBorderWidth command', () => {
+			expect( editor.commands.get( 'tableCellBorderWidth' ) ).to.be.instanceOf( TableCellBorderWidthCommand );
+		} );
+
+		it( 'adds tableCellAlignment command', () => {
+			expect( editor.commands.get( 'tableCellHorizontalAlignment' ) ).to.be.instanceOf( TableCellHorizontalAlignmentCommand );
+		} );
+
+		it( 'adds tableCellAlignment command', () => {
+			expect( editor.commands.get( 'tableCellVerticalAlignment' ) ).to.be.instanceOf( TableCellVerticalAlignmentCommand );
+		} );
+
+		it( 'adds tableCellPadding command', () => {
+			expect( editor.commands.get( 'tableCellPadding' ) ).to.be.instanceOf( TableCellPaddingCommand );
+		} );
+
+		it( 'adds tableCellBackgroundColor command', () => {
+			expect( editor.commands.get( 'tableCellBackgroundColor' ) ).to.be.instanceOf( TableCellBackgroundColorCommand );
+		} );
+
+		it( 'adds tableCellWidth command', () => {
+			expect( editor.commands.get( 'tableCellWidth' ) ).to.be.instanceOf( TableCellWidthCommand );
+		} );
+
+		it( 'adds tableCellHeight command', () => {
+			expect( editor.commands.get( 'tableCellHeight' ) ).to.be.instanceOf( TableCellHeightCommand );
+		} );
+
+		describe( 'border', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'borderColor' ) ).to.be.true;
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'borderStyle' ) ).to.be.true;
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'borderWidth' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast border shorthand', () => {
+					editor.setData( '<table><tr><td style="border:1px solid #f00">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', '#f00' );
+					assertTRBLAttribute( tableCell, 'borderStyle', 'solid' );
+					assertTRBLAttribute( tableCell, 'borderWidth', '1px' );
+				} );
+
+				it( 'should upcast border-color shorthand', () => {
+					editor.setData( '<table><tr><td style="border-color:#f00">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', '#f00' );
+				} );
+
+				it( 'should upcast border-style shorthand', () => {
+					editor.setData( '<table><tr><td style="border-style:ridge">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderStyle', 'ridge' );
+				} );
+
+				it( 'should upcast border-width shorthand', () => {
+					editor.setData( '<table><tr><td style="border-width:1px">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderWidth', '1px' );
+				} );
+
+				it( 'should upcast border-top shorthand', () => {
+					editor.setData( '<table><tr><td style="border-top:1px solid #f00">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', '#f00', null, null, null );
+					assertTRBLAttribute( tableCell, 'borderStyle', 'solid', null, null, null );
+					assertTRBLAttribute( tableCell, 'borderWidth', '1px', null, null, null );
+				} );
+
+				it( 'should upcast border-right shorthand', () => {
+					editor.setData( '<table><tr><td style="border-right:1px solid #f00">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', null, '#f00', null, null );
+					assertTRBLAttribute( tableCell, 'borderStyle', null, 'solid', null, null );
+					assertTRBLAttribute( tableCell, 'borderWidth', null, '1px', null, null );
+				} );
+
+				it( 'should upcast border-bottom shorthand', () => {
+					editor.setData( '<table><tr><td style="border-bottom:1px solid #f00">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', null, null, '#f00', null );
+					assertTRBLAttribute( tableCell, 'borderStyle', null, null, 'solid', null );
+					assertTRBLAttribute( tableCell, 'borderWidth', null, null, '1px', null );
+				} );
+
+				it( 'should upcast border-left shorthand', () => {
+					editor.setData( '<table><tr><td style="border-left:1px solid #f00">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', null, null, null, '#f00' );
+					assertTRBLAttribute( tableCell, 'borderStyle', null, null, null, 'solid' );
+					assertTRBLAttribute( tableCell, 'borderWidth', null, null, null, '1px' );
+				} );
+
+				it( 'should upcast mixed shorthands', () => {
+					editor.setData(
+						'<table><tr><td style="border-top:1px solid #f00;border-bottom:2em ridge rgba(255,0,0,1)">foo</td></tr></table>'
+					);
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', '#f00', null, 'rgba(255, 0, 0, 1)', null );
+					assertTRBLAttribute( tableCell, 'borderStyle', 'solid', null, 'ridge', null );
+					assertTRBLAttribute( tableCell, 'borderWidth', '1px', null, '2em', null );
+				} );
+
+				it( 'should upcast border-top-* styles', () => {
+					editor.setData(
+						'<table><tr><td style="border-top-width:1px;border-top-style:solid;border-top-color:#f00">foo</td></tr></table>'
+					);
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', '#f00', null, null, null );
+					assertTRBLAttribute( tableCell, 'borderStyle', 'solid', null, null, null );
+					assertTRBLAttribute( tableCell, 'borderWidth', '1px', null, null, null );
+				} );
+
+				it( 'should upcast border-right-* styles', () => {
+					editor.setData(
+						'<table>' +
+							'<tr>' +
+								'<td style="border-right-width:1px;border-right-style:solid;border-right-color:#f00">foo</td>' +
+							'</tr>' +
+						'</table>'
+					);
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', null, '#f00', null, null );
+					assertTRBLAttribute( tableCell, 'borderStyle', null, 'solid', null, null );
+					assertTRBLAttribute( tableCell, 'borderWidth', null, '1px', null, null );
+				} );
+
+				it( 'should upcast border-bottom-* styles', () => {
+					editor.setData(
+						'<table>' +
+							'<tr>' +
+								'<td style="border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#f00">foo</td>' +
+							'</tr>' +
+						'</table>'
+					);
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', null, null, '#f00', null );
+					assertTRBLAttribute( tableCell, 'borderStyle', null, null, 'solid', null );
+					assertTRBLAttribute( tableCell, 'borderWidth', null, null, '1px', null );
+				} );
+
+				it( 'should upcast border-left-* styles', () => {
+					editor.setData(
+						'<table>' +
+							'<tr>' +
+								'<td style="border-left-width:1px;border-left-style:solid;border-left-color:#f00">foo</td>' +
+							'</tr>' +
+						'</table>'
+					);
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'borderColor', null, null, null, '#f00' );
+					assertTRBLAttribute( tableCell, 'borderStyle', null, null, null, 'solid' );
+					assertTRBLAttribute( tableCell, 'borderWidth', null, null, null, '1px' );
+				} );
+
+				it( 'should allow to be overriden (only border-top consumed)', () => {
+					editor.conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:td', ( evt, data, conversionApi ) => {
+						conversionApi.consumable.consume( data.viewItem, {
+							styles: [ 'border-top' ]
+						} );
+					}, { priority: 'high' } ) );
+
+					editor.setData( '<table><tr><td style="border:1px solid blue;">foo</td></tr></table>' );
+
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'borderColor' ) ).to.be.undefined;
+					expect( tableCell.getAttribute( 'borderStyle' ) ).to.be.undefined;
+					expect( tableCell.getAttribute( 'borderWidth' ) ).to.be.undefined;
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let tableCell;
+
+				beforeEach( () => {
+					setModelData(
+						model,
+						'<table headingRows="0" headingColumns="0">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>'
+					);
+					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				} );
+
+				it( 'should downcast borderColor attribute (same top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderColor', {
+						top: '#f00',
+						right: '#f00',
+						bottom: '#f00',
+						left: '#f00'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+				} );
+
+				it( 'should downcast borderColor attribute (different top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderColor', {
+						top: '#f00',
+						right: 'hsla(0, 100%, 50%, 0.5)',
+						bottom: 'deeppink',
+						left: 'rgb(255, 0, 0)'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor,
+						'border-bottom:deeppink;' +
+						'border-left:rgb(255, 0, 0);' +
+						'border-right:hsla(0, 100%, 50%, 0.5);' +
+						'border-top:#f00;'
+					);
+				} );
+
+				it( 'should downcast borderStyle attribute (same top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderStyle', {
+						top: 'solid',
+						right: 'solid',
+						bottom: 'solid',
+						left: 'solid'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+				} );
+
+				it( 'should downcast borderStyle attribute (different top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderStyle', {
+						top: 'solid',
+						right: 'ridge',
+						bottom: 'dotted',
+						left: 'dashed'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor, 'border-bottom:dotted;border-left:dashed;border-right:ridge;border-top:solid;' );
+				} );
+
+				it( 'should downcast borderWidth attribute (same top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderWidth', {
+						top: '42px',
+						right: '.1em',
+						bottom: '1337rem',
+						left: 'thick'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor, 'border-bottom:1337rem;border-left:thick;border-right:.1em;border-top:42px;' );
+				} );
+
+				it( 'should downcast borderWidth attribute (different top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderWidth', {
+						top: '42px',
+						right: '42px',
+						bottom: '42px',
+						left: '42px'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor, 'border-bottom:42px;border-left:42px;border-right:42px;border-top:42px;' );
+				} );
+
+				it( 'should downcast borderColor, borderStyle and borderWidth attributes together (same top, right, bottom, left)', () => {
+					model.change( writer => {
+						writer.setAttribute( 'borderColor', {
+							top: '#f00',
+							right: '#f00',
+							bottom: '#f00',
+							left: '#f00'
+						}, tableCell );
+
+						writer.setAttribute( 'borderStyle', {
+							top: 'solid',
+							right: 'solid',
+							bottom: 'solid',
+							left: 'solid'
+						}, tableCell );
+
+						writer.setAttribute( 'borderWidth', {
+							top: '42px',
+							right: '42px',
+							bottom: '42px',
+							left: '42px'
+						}, tableCell );
+					} );
+
+					assertTableCellStyle( editor,
+						'border-bottom:42px solid #f00;' +
+						'border-left:42px solid #f00;' +
+						'border-right:42px solid #f00;' +
+						'border-top:42px solid #f00;'
+					);
+				} );
+
+				it(
+					'should downcast borderColor, borderStyle and borderWidth attributes together (different top, right, bottom, left)',
+					() => {
+						model.change( writer => {
+							writer.setAttribute( 'borderColor', {
+								top: '#f00',
+								right: 'hsla(0, 100%, 50%, 0.5)',
+								bottom: 'deeppink',
+								left: 'rgb(255, 0, 0)'
+							}, tableCell );
+
+							writer.setAttribute( 'borderStyle', {
+								top: 'solid',
+								right: 'ridge',
+								bottom: 'dotted',
+								left: 'dashed'
+							}, tableCell );
+
+							writer.setAttribute( 'borderWidth', {
+								top: '42px',
+								right: '.1em',
+								bottom: '1337rem',
+								left: 'thick'
+							}, tableCell );
+						} );
+
+						assertTableCellStyle( editor,
+							'border-bottom:1337rem dotted deeppink;' +
+							'border-left:thick dashed rgb(255, 0, 0);' +
+							'border-right:.1em ridge hsla(0, 100%, 50%, 0.5);' +
+							'border-top:42px solid #f00;'
+						);
+					}
+				);
+
+				describe( 'change attribute', () => {
+					beforeEach( () => {
+						model.change( writer => {
+							writer.setAttribute( 'borderColor', {
+								top: '#f00',
+								right: '#f00',
+								bottom: '#f00',
+								left: '#f00'
+							}, tableCell );
+
+							writer.setAttribute( 'borderStyle', {
+								top: 'solid',
+								right: 'solid',
+								bottom: 'solid',
+								left: 'solid'
+							}, tableCell );
+
+							writer.setAttribute( 'borderWidth', {
+								top: '42px',
+								right: '42px',
+								bottom: '42px',
+								left: '42px'
+							}, tableCell );
+						} );
+					} );
+
+					it( 'should downcast borderColor attribute change', () => {
+						model.change( writer => writer.setAttribute( 'borderColor', {
+							top: 'deeppink',
+							right: 'deeppink',
+							bottom: 'deeppink',
+							left: 'deeppink'
+						}, tableCell ) );
+
+						assertTableCellStyle( editor,
+							'border-bottom:42px solid deeppink;' +
+							'border-left:42px solid deeppink;' +
+							'border-right:42px solid deeppink;' +
+							'border-top:42px solid deeppink;'
+						);
+					} );
+
+					it( 'should downcast borderStyle attribute change', () => {
+						model.change( writer => writer.setAttribute( 'borderStyle', {
+							top: 'ridge',
+							right: 'ridge',
+							bottom: 'ridge',
+							left: 'ridge'
+						}, tableCell ) );
+
+						assertTableCellStyle( editor,
+							'border-bottom:42px ridge #f00;' +
+							'border-left:42px ridge #f00;' +
+							'border-right:42px ridge #f00;' +
+							'border-top:42px ridge #f00;'
+						);
+					} );
+
+					it( 'should downcast borderWidth attribute change', () => {
+						model.change( writer => writer.setAttribute( 'borderWidth', {
+							top: 'thick',
+							right: 'thick',
+							bottom: 'thick',
+							left: 'thick'
+						}, tableCell ) );
+
+						assertTableCellStyle( editor,
+							'border-bottom:thick solid #f00;' +
+							'border-left:thick solid #f00;' +
+							'border-right:thick solid #f00;' +
+							'border-top:thick solid #f00;'
+						);
+					} );
+
+					it( 'should downcast borderColor attribute removal', () => {
+						model.change( writer => writer.removeAttribute( 'borderColor', tableCell ) );
+
+						assertTableCellStyle( editor,
+							'border-bottom:42px solid;' +
+							'border-left:42px solid;' +
+							'border-right:42px solid;' +
+							'border-top:42px solid;'
+						);
+					} );
+
+					it( 'should downcast borderStyle attribute removal', () => {
+						model.change( writer => writer.removeAttribute( 'borderStyle', tableCell ) );
+
+						assertTableCellStyle( editor,
+							'border-bottom:42px #f00;' +
+							'border-left:42px #f00;' +
+							'border-right:42px #f00;' +
+							'border-top:42px #f00;'
+						);
+					} );
+
+					it( 'should downcast borderWidth attribute removal', () => {
+						model.change( writer => writer.removeAttribute( 'borderWidth', tableCell ) );
+
+						assertTableCellStyle( editor,
+							'border-bottom:solid #f00;' +
+							'border-left:solid #f00;' +
+							'border-right:solid #f00;' +
+							'border-top:solid #f00;'
+						);
+					} );
+
+					it( 'should downcast borderColor, borderStyle and borderWidth attributes removal', () => {
+						model.change( writer => {
+							writer.removeAttribute( 'borderColor', tableCell );
+							writer.removeAttribute( 'borderStyle', tableCell );
+							writer.removeAttribute( 'borderWidth', tableCell );
+						} );
+
+						assertEqualMarkup(
+							editor.getData(),
+							'<figure class="table"><table><tbody><tr><td>foo</td></tr></tbody></table></figure>'
+						);
+					} );
+				} );
+			} );
+		} );
+
+		describe( 'background color', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'backgroundColor' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast background-color', () => {
+					editor.setData( '<table><tr><td style="background-color:#f00">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let tableCell;
+
+				beforeEach( () => {
+					setModelData(
+						model,
+						'<table headingRows="0" headingColumns="0">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>'
+					);
+					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				} );
+
+				it( 'should downcast backgroundColor', () => {
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', tableCell ) );
+
+					assertTableCellStyle( editor, 'background-color:#f00;' );
+				} );
+			} );
+		} );
+
+		describe( 'horizontal alignment', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'horizontalAlignment' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast text-align:left style', () => {
+					editor.setData( '<table><tr><td style="text-align:left">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'left' );
+				} );
+
+				it( 'should upcast text-align:right style', () => {
+					editor.setData( '<table><tr><td style="text-align:right">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'right' );
+				} );
+
+				it( 'should upcast text-align:center style', () => {
+					editor.setData( '<table><tr><td style="text-align:center">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'center' );
+				} );
+
+				it( 'should upcast text-align:justify style', () => {
+					editor.setData( '<table><tr><td style="text-align:justify">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'horizontalAlignment' ) ).to.equal( 'justify' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let tableCell;
+
+				beforeEach( () => {
+					setModelData(
+						model,
+						'<table headingRows="0" headingColumns="0">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>'
+					);
+					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				} );
+
+				it( 'should downcast horizontalAlignment=left', () => {
+					model.change( writer => writer.setAttribute( 'horizontalAlignment', 'left', tableCell ) );
+
+					assertTableCellStyle( editor, 'text-align:left;' );
+				} );
+
+				it( 'should downcast horizontalAlignment=right', () => {
+					model.change( writer => writer.setAttribute( 'horizontalAlignment', 'right', tableCell ) );
+
+					assertTableCellStyle( editor, 'text-align:right;' );
+				} );
+
+				it( 'should downcast horizontalAlignment=center', () => {
+					model.change( writer => writer.setAttribute( 'horizontalAlignment', 'center', tableCell ) );
+
+					assertTableCellStyle( editor, 'text-align:center;' );
+				} );
+
+				it( 'should downcast horizontalAlignment=justify', () => {
+					model.change( writer => writer.setAttribute( 'horizontalAlignment', 'justify', tableCell ) );
+
+					assertTableCellStyle( editor, 'text-align:justify;' );
+				} );
+			} );
+		} );
+
+		describe( 'vertical alignment', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'verticalAlignment' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast vertical-align', () => {
+					editor.setData( '<table><tr><td style="vertical-align:top">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'verticalAlignment' ) ).to.equal( 'top' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let tableCell;
+
+				beforeEach( () => {
+					setModelData(
+						model,
+						'<table headingRows="0" headingColumns="0">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>'
+					);
+					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				} );
+
+				it( 'should downcast verticalAlignment', () => {
+					model.change( writer => writer.setAttribute( 'verticalAlignment', 'middle', tableCell ) );
+
+					assertTableCellStyle( editor, 'vertical-align:middle;' );
+				} );
+			} );
+		} );
+
+		describe( 'padding', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'padding' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast padding shorthand', () => {
+					editor.setData( '<table><tr><td style="padding:2px 4em">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					assertTRBLAttribute( tableCell, 'padding', '2px', '4em' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let tableCell;
+
+				beforeEach( () => {
+					setModelData(
+						model,
+						'<table headingRows="0" headingColumns="0">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>'
+					);
+					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				} );
+
+				it( 'should downcast padding (same top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'padding', {
+						top: '2px',
+						right: '2px',
+						bottom: '2px',
+						left: '2px'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor, 'padding:2px;' );
+				} );
+
+				it( 'should downcast padding (different top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'padding', {
+						top: '2px',
+						right: '3px',
+						bottom: '4px',
+						left: '5px'
+					}, tableCell ) );
+
+					assertTableCellStyle( editor, 'padding:2px 3px 4px 5px;' );
+				} );
+			} );
+		} );
+
+		describe( 'cell width', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'width' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast width attribute on table cell', () => {
+					editor.setData( '<table><tr><td style="width:20px">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'width' ) ).to.equal( '20px' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let tableCell;
+
+				beforeEach( () => {
+					setModelData(
+						model,
+						'<table headingRows="0" headingColumns="0">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>'
+					);
+
+					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				} );
+
+				it( 'should downcast width attribute', () => {
+					model.change( writer => writer.setAttribute( 'width', '20px', tableCell ) );
+
+					assertEqualMarkup(
+						editor.getData(),
+						'<figure class="table"><table><tbody><tr><td style="width:20px;">foo</td></tr></tbody></table></figure>'
+					);
+				} );
+			} );
+		} );
+
+		describe( 'cell height', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'height' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast height attribute on table cell', () => {
+					editor.setData( '<table><tr><td style="height:20px">foo</td></tr></table>' );
+					const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+
+					expect( tableCell.getAttribute( 'height' ) ).to.equal( '20px' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let tableCell;
+
+				beforeEach( () => {
+					setModelData(
+						model,
+						'<table headingRows="0" headingColumns="0">' +
+							'<tableRow>' +
+								'<tableCell>' +
+									'<paragraph>foo</paragraph>' +
+								'</tableCell>' +
+							'</tableRow>' +
+						'</table>'
+					);
+
+					tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
+				} );
+
+				it( 'should downcast height attribute', () => {
+					model.change( writer => writer.setAttribute( 'height', '20px', tableCell ) );
+
+					assertEqualMarkup(
+						editor.getData(),
+						'<figure class="table"><table><tbody><tr><td style="height:20px;">foo</td></tr></tbody></table></figure>'
+					);
+				} );
+			} );
+		} );
+	} );
+} );

+ 0 - 123
packages/ckeditor5-table/tests/tablecolumnrowproperties.js

@@ -1,123 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-
-import TableEditing from '../src/tableediting';
-import TableColumnRowProperties from '../src/tablecolumnrowproperties';
-
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
-
-describe( 'TableColumnRowProperties', () => {
-	let editor, model;
-
-	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
-				plugins: [ TableColumnRowProperties, Paragraph, TableEditing ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-
-				model = editor.model;
-			} );
-	} );
-
-	afterEach( () => {
-		editor.destroy();
-	} );
-
-	it( 'should have pluginName', () => {
-		expect( TableColumnRowProperties.pluginName ).to.equal( 'TableColumnRowProperties' );
-	} );
-
-	describe( 'column width', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'width' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast width attribute on table cell', () => {
-				editor.setData( '<table><tr><td style="width:20px">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'width' ) ).to.equal( '20px' );
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let tableCell;
-
-			beforeEach( () => {
-				setModelData(
-					model,
-					'<table headingRows="0" headingColumns="0">' +
-					'<tableRow>' +
-					'<tableCell>' +
-					'<paragraph>foo</paragraph>' +
-					'</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-
-				tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-			} );
-
-			it( 'should downcast width attribute', () => {
-				model.change( writer => writer.setAttribute( 'width', '20px', tableCell ) );
-
-				assertEqualMarkup(
-					editor.getData(),
-					'<figure class="table"><table><tbody><tr><td style="width:20px;">foo</td></tr></tbody></table></figure>'
-				);
-			} );
-		} );
-	} );
-
-	describe( 'row height', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'tableCell' ], 'height' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast height attribute on table cell', () => {
-				editor.setData( '<table><tr><td style="height:20px">foo</td></tr></table>' );
-				const tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-
-				expect( tableCell.getAttribute( 'height' ) ).to.equal( '20px' );
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let tableCell;
-
-			beforeEach( () => {
-				setModelData(
-					model,
-					'<table headingRows="0" headingColumns="0">' +
-					'<tableRow>' +
-					'<tableCell>' +
-					'<paragraph>foo</paragraph>' +
-					'</tableCell>' +
-					'</tableRow>' +
-					'</table>'
-				);
-
-				tableCell = model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] );
-			} );
-
-			it( 'should downcast height attribute', () => {
-				model.change( writer => writer.setAttribute( 'height', '20px', tableCell ) );
-
-				assertEqualMarkup(
-					editor.getData(),
-					'<figure class="table"><table><tbody><tr><td style="height:20px;">foo</td></tr></tbody></table></figure>'
-				);
-			} );
-		} );
-	} );
-} );

+ 21 - 647
packages/ckeditor5-table/tests/tableproperties.js

@@ -1,670 +1,44 @@
 /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * @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 global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 
 import TableEditing from '../src/tableediting';
 import TableProperties from '../src/tableproperties';
-import { assertTableStyle, assertTRBLAttribute } from './_utils/utils';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import TablePropertiesEditing from '../src/tableproperties/tablepropertiesediting';
 
-describe( 'TableProperties', () => {
-	let editor, model;
+describe( 'table properties', () => {
+	describe( 'TableProperties', () => {
+		let editor, editorElement;
 
-	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
-				plugins: [ TableProperties, Paragraph, TableEditing ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-
-				model = editor.model;
-			} );
-	} );
-
-	afterEach( () => {
-		editor.destroy();
-	} );
-
-	it( 'should have pluginName', () => {
-		expect( TableProperties.pluginName ).to.equal( 'TableProperties' );
-	} );
-
-	describe( 'border', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'table' ], 'borderColor' ) ).to.be.true;
-			expect( model.schema.checkAttribute( [ '$root', 'table' ], 'borderStyle' ) ).to.be.true;
-			expect( model.schema.checkAttribute( [ '$root', 'table' ], 'borderWidth' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast border shorthand', () => {
-				editor.setData( '<table style="border:1px solid #f00"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', '#f00' );
-				assertTRBLAttribute( table, 'borderStyle', 'solid' );
-				assertTRBLAttribute( table, 'borderWidth', '1px' );
-			} );
-
-			it( 'should upcast border-color shorthand', () => {
-				editor.setData( '<table style="border-color:#f00"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', '#f00' );
-			} );
-
-			it( 'should upcast border-style shorthand', () => {
-				editor.setData( '<table style="border-style:ridge"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderStyle', 'ridge' );
-			} );
-
-			it( 'should upcast border-width shorthand', () => {
-				editor.setData( '<table style="border-width:1px"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderWidth', '1px' );
-			} );
-
-			it( 'should upcast border-top shorthand', () => {
-				editor.setData( '<table style="border-top:1px solid #f00"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', '#f00', null, null, null );
-				assertTRBLAttribute( table, 'borderStyle', 'solid', null, null, null );
-				assertTRBLAttribute( table, 'borderWidth', '1px', null, null, null );
-			} );
-
-			it( 'should upcast border-right shorthand', () => {
-				editor.setData( '<table style="border-right:1px solid #f00"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', null, '#f00', null, null );
-				assertTRBLAttribute( table, 'borderStyle', null, 'solid', null, null );
-				assertTRBLAttribute( table, 'borderWidth', null, '1px', null, null );
-			} );
-
-			it( 'should upcast border-bottom shorthand', () => {
-				editor.setData( '<table style="border-bottom:1px solid #f00"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', null, null, '#f00', null );
-				assertTRBLAttribute( table, 'borderStyle', null, null, 'solid', null );
-				assertTRBLAttribute( table, 'borderWidth', null, null, '1px', null );
-			} );
-
-			it( 'should upcast border-left shorthand', () => {
-				editor.setData( '<table style="border-left:1px solid #f00"><tr><td>foo</td></tr></table>' );
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', null, null, null, '#f00' );
-				assertTRBLAttribute( table, 'borderStyle', null, null, null, 'solid' );
-				assertTRBLAttribute( table, 'borderWidth', null, null, null, '1px' );
-			} );
-
-			it( 'should upcast border-top-* styles', () => {
-				editor.setData(
-					'<table style="border-top-width:1px;border-top-style:solid;border-top-color:#f00"><tr><td>foo</td></tr></table>'
-				);
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', '#f00', null, null, null );
-				assertTRBLAttribute( table, 'borderStyle', 'solid', null, null, null );
-				assertTRBLAttribute( table, 'borderWidth', '1px', null, null, null );
-			} );
-
-			it( 'should upcast border-right-* styles', () => {
-				editor.setData(
-					'<table style="border-right-width:1px;border-right-style:solid;border-right-color:#f00"><tr><td>foo</td></tr></table>'
-				);
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', null, '#f00', null, null );
-				assertTRBLAttribute( table, 'borderStyle', null, 'solid', null, null );
-				assertTRBLAttribute( table, 'borderWidth', null, '1px', null, null );
-			} );
-
-			it( 'should upcast border-bottom-* styles', () => {
-				editor.setData(
-					'<table style="border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#f00">' +
-					'<tr>' +
-					'<td>foo</td>' +
-					'</tr>' +
-					'</table>'
-				);
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', null, null, '#f00', null );
-				assertTRBLAttribute( table, 'borderStyle', null, null, 'solid', null );
-				assertTRBLAttribute( table, 'borderWidth', null, null, '1px', null );
-			} );
-
-			it( 'should upcast border-left-* styles', () => {
-				editor.setData(
-					'<table style="border-left-width:1px;border-left-style:solid;border-left-color:#f00"><tr><td>foo</td></tr></table>'
-				);
-
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				assertTRBLAttribute( table, 'borderColor', null, null, null, '#f00' );
-				assertTRBLAttribute( table, 'borderStyle', null, null, null, 'solid' );
-				assertTRBLAttribute( table, 'borderWidth', null, null, null, '1px' );
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let table;
-
-			beforeEach( () => {
-				table = createEmptyTable();
-			} );
-
-			it( 'should downcast borderColor attribute (same top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderColor', {
-					top: '#f00',
-					right: '#f00',
-					bottom: '#f00',
-					left: '#f00'
-				}, table ) );
-
-				assertTableStyle( editor, 'border-top:#f00;border-right:#f00;border-bottom:#f00;border-left:#f00;' );
-			} );
-
-			it( 'should downcast borderColor attribute (different top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderColor', {
-					top: '#f00',
-					right: 'hsla(0, 100%, 50%, 0.5)',
-					bottom: 'deeppink',
-					left: 'rgb(255, 0, 0)'
-				}, table ) );
-
-				assertTableStyle( editor,
-					'border-top:#f00;' +
-					'border-right:hsla(0, 100%, 50%, 0.5);' +
-					'border-bottom:deeppink;' +
-					'border-left:rgb(255, 0, 0);'
-				);
-			} );
-
-			it( 'should downcast borderStyle attribute (same top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderStyle', {
-					top: 'solid',
-					right: 'solid',
-					bottom: 'solid',
-					left: 'solid'
-				}, table ) );
-
-				assertTableStyle( editor, 'border-top:solid;border-right:solid;border-bottom:solid;border-left:solid;' );
-			} );
-
-			it( 'should downcast borderStyle attribute (different top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderStyle', {
-					top: 'solid',
-					right: 'ridge',
-					bottom: 'dotted',
-					left: 'dashed'
-				}, table ) );
-
-				assertTableStyle( editor, 'border-top:solid;border-right:ridge;border-bottom:dotted;border-left:dashed;' );
-			} );
-
-			it( 'should downcast borderWidth attribute (same top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderWidth', {
-					top: '42px',
-					right: '.1em',
-					bottom: '1337rem',
-					left: 'thick'
-				}, table ) );
-
-				assertTableStyle( editor, 'border-top:42px;border-right:.1em;border-bottom:1337rem;border-left:thick;' );
-			} );
-
-			it( 'should downcast borderWidth attribute (different top, right, bottom, left)', () => {
-				model.change( writer => writer.setAttribute( 'borderWidth', {
-					top: '42px',
-					right: '42px',
-					bottom: '42px',
-					left: '42px'
-				}, table ) );
-
-				assertTableStyle( editor, 'border-top:42px;border-right:42px;border-bottom:42px;border-left:42px;' );
-			} );
-
-			it( 'should downcast borderColor, borderStyle and borderWidth attributes together (same top, right, bottom, left)', () => {
-				model.change( writer => {
-					writer.setAttribute( 'borderColor', {
-						top: '#f00',
-						right: '#f00',
-						bottom: '#f00',
-						left: '#f00'
-					}, table );
-
-					writer.setAttribute( 'borderStyle', {
-						top: 'solid',
-						right: 'solid',
-						bottom: 'solid',
-						left: 'solid'
-					}, table );
-
-					writer.setAttribute( 'borderWidth', {
-						top: '42px',
-						right: '42px',
-						bottom: '42px',
-						left: '42px'
-					}, table );
-				} );
-
-				assertTableStyle( editor,
-					'border-top:42px solid #f00;' +
-					'border-right:42px solid #f00;' +
-					'border-bottom:42px solid #f00;' +
-					'border-left:42px solid #f00;'
-				);
-			} );
-
-			it( 'should downcast borderColor, borderStyle and borderWidth attributes together (different top, right, bottom, left)', () => {
-				model.change( writer => {
-					writer.setAttribute( 'borderColor', {
-						top: '#f00',
-						right: 'hsla(0, 100%, 50%, 0.5)',
-						bottom: 'deeppink',
-						left: 'rgb(255, 0, 0)'
-					}, table );
-
-					writer.setAttribute( 'borderStyle', {
-						top: 'solid',
-						right: 'ridge',
-						bottom: 'dotted',
-						left: 'dashed'
-					}, table );
-
-					writer.setAttribute( 'borderWidth', {
-						top: '42px',
-						right: '.1em',
-						bottom: '1337rem',
-						left: 'thick'
-					}, table );
-				} );
-
-				assertTableStyle( editor,
-					'border-top:42px solid #f00;' +
-					'border-right:.1em ridge hsla(0, 100%, 50%, 0.5);' +
-					'border-bottom:1337rem dotted deeppink;' +
-					'border-left:thick dashed rgb(255, 0, 0);'
-				);
-			} );
-
-			describe( 'change attribute', () => {
-				beforeEach( () => {
-					model.change( writer => {
-						writer.setAttribute( 'borderColor', {
-							top: '#f00',
-							right: '#f00',
-							bottom: '#f00',
-							left: '#f00'
-						}, table );
-
-						writer.setAttribute( 'borderStyle', {
-							top: 'solid',
-							right: 'solid',
-							bottom: 'solid',
-							left: 'solid'
-						}, table );
-
-						writer.setAttribute( 'borderWidth', {
-							top: '42px',
-							right: '42px',
-							bottom: '42px',
-							left: '42px'
-						}, table );
-					} );
-				} );
-
-				it( 'should downcast borderColor attribute change', () => {
-					model.change( writer => writer.setAttribute( 'borderColor', {
-						top: 'deeppink',
-						right: 'deeppink',
-						bottom: 'deeppink',
-						left: 'deeppink'
-					}, table ) );
-
-					assertTableStyle( editor,
-						'border-top:42px solid deeppink;' +
-						'border-right:42px solid deeppink;' +
-						'border-bottom:42px solid deeppink;' +
-						'border-left:42px solid deeppink;'
-					);
-				} );
-
-				it( 'should downcast borderStyle attribute change', () => {
-					model.change( writer => writer.setAttribute( 'borderStyle', {
-						top: 'ridge',
-						right: 'ridge',
-						bottom: 'ridge',
-						left: 'ridge'
-					}, table ) );
-
-					assertTableStyle( editor,
-						'border-top:42px ridge #f00;' +
-						'border-right:42px ridge #f00;' +
-						'border-bottom:42px ridge #f00;' +
-						'border-left:42px ridge #f00;'
-					);
-				} );
-
-				it( 'should downcast borderWidth attribute change', () => {
-					model.change( writer => writer.setAttribute( 'borderWidth', {
-						top: 'thick',
-						right: 'thick',
-						bottom: 'thick',
-						left: 'thick'
-					}, table ) );
-
-					assertTableStyle( editor,
-						'border-top:thick solid #f00;' +
-						'border-right:thick solid #f00;' +
-						'border-bottom:thick solid #f00;' +
-						'border-left:thick solid #f00;'
-					);
-				} );
-
-				it( 'should downcast borderColor attribute removal', () => {
-					model.change( writer => writer.removeAttribute( 'borderColor', table ) );
-
-					assertTableStyle( editor,
-						'border-top:42px solid;' +
-						'border-right:42px solid;' +
-						'border-bottom:42px solid;' +
-						'border-left:42px solid;'
-					);
-				} );
-
-				it( 'should downcast borderStyle attribute removal', () => {
-					model.change( writer => writer.removeAttribute( 'borderStyle', table ) );
+		beforeEach( async () => {
+			editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
 
-					assertTableStyle( editor,
-						'border-top:42px #f00;' +
-						'border-right:42px #f00;' +
-						'border-bottom:42px #f00;' +
-						'border-left:42px #f00;'
-					);
-				} );
-
-				it( 'should downcast borderWidth attribute removal', () => {
-					model.change( writer => writer.removeAttribute( 'borderWidth', table ) );
-
-					assertTableStyle( editor,
-						'border-top:solid #f00;' +
-						'border-right:solid #f00;' +
-						'border-bottom:solid #f00;' +
-						'border-left:solid #f00;'
-					);
-				} );
-
-				it( 'should downcast borderColor, borderStyle and borderWidth attributes removal', () => {
-					model.change( writer => {
-						writer.removeAttribute( 'borderColor', table );
-						writer.removeAttribute( 'borderStyle', table );
-						writer.removeAttribute( 'borderWidth', table );
-					} );
-
-					assertEqualMarkup(
-						editor.getData(),
-						'<figure class="table"><table><tbody><tr><td>foo</td></tr></tbody></table></figure>'
-					);
-				} );
-			} );
-		} );
-	} );
-
-	describe( 'background color', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'table' ], 'backgroundColor' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast background-color', () => {
-				editor.setData( '<table style="background-color:#f00"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
-			} );
-
-			it( 'should upcast from background shorthand', () => {
-				editor.setData( '<table style="background:#f00 center center"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let table;
-
-			beforeEach( () => {
-				table = createEmptyTable();
-			} );
-
-			it( 'should downcast backgroundColor', () => {
-				model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', table ) );
-
-				assertTableStyle( editor, 'background-color:#f00;' );
-			} );
-		} );
-	} );
-
-	describe( 'width', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'table' ], 'width' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast width', () => {
-				editor.setData( '<table style="width:1337px"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'width' ) ).to.equal( '1337px' );
+			editor = await ClassicTestEditor.create( editorElement, {
+				plugins: [ TableProperties, Paragraph, TableEditing ]
 			} );
 		} );
 
-		describe( 'downcast conversion', () => {
-			let table;
-
-			beforeEach( () => {
-				table = createEmptyTable();
-			} );
-
-			it( 'should downcast width', () => {
-				model.change( writer => writer.setAttribute( 'width', '1337px', table ) );
-
-				assertTableStyle( editor, 'width:1337px;' );
-			} );
+		afterEach( async () => {
+			editorElement.remove();
+			await editor.destroy();
 		} );
-	} );
 
-	describe( 'height', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'table' ], 'height' ) ).to.be.true;
+		it( 'should be loaded', () => {
+			expect( editor.plugins.get( TableProperties ) ).to.instanceOf( TableProperties );
 		} );
 
-		describe( 'upcast conversion', () => {
-			it( 'should upcast height', () => {
-				editor.setData( '<table style="height:1337px"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'height' ) ).to.equal( '1337px' );
-			} );
+		it( 'should load TablePropertiesEditing plugin', () => {
+			expect( editor.plugins.get( TablePropertiesEditing ) ).to.instanceOf( TablePropertiesEditing );
 		} );
 
-		describe( 'downcast conversion', () => {
-			let table;
-
-			beforeEach( () => {
-				table = createEmptyTable();
-			} );
-
-			it( 'should downcast height', () => {
-				model.change( writer => writer.setAttribute( 'height', '1337px', table ) );
-
-				assertTableStyle( editor, 'height:1337px;' );
-			} );
+		it( 'should have pluginName', () => {
+			expect( TableProperties.pluginName ).to.equal( 'TableProperties' );
 		} );
 	} );
-
-	describe( 'alignment', () => {
-		it( 'should set proper schema rules', () => {
-			expect( model.schema.checkAttribute( [ '$root', 'table' ], 'alignment' ) ).to.be.true;
-		} );
-
-		describe( 'upcast conversion', () => {
-			it( 'should upcast style="margin-left:auto;margin-right:0" to right value', () => {
-				editor.setData( '<table style="margin-left:auto;margin-right:0"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
-			} );
-
-			it( 'should upcast style="margin-left:0;margin-right:auto" to left value', () => {
-				editor.setData( '<table style="margin-left:0;margin-right:auto"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'left' );
-			} );
-
-			it( 'should upcast style="margin-left:auto;margin-right:auto" to center value', () => {
-				editor.setData( '<table style="margin-left:auto;margin-right:auto"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'center' );
-			} );
-
-			it( 'should upcast style="margin-left:auto;margin-right:0pt" to right value', () => {
-				editor.setData( '<table style="margin-left:auto;margin-right:0pt"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
-			} );
-
-			it( 'should upcast style="margin-left:auto;margin-right:0%" to right value', () => {
-				editor.setData( '<table style="margin-left:auto;margin-right:0%"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
-			} );
-
-			it( 'should not upcast style="margin-left:auto;margin-right:0.23pt" to right value', () => {
-				editor.setData( '<table style="margin-left:auto;margin-right:0.23pt"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.hasAttribute( 'alignment' ) ).to.be.false;
-			} );
-
-			it( 'should upcast align=right attribute', () => {
-				editor.setData( '<table align="right"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
-			} );
-
-			it( 'should upcast align=left attribute', () => {
-				editor.setData( '<table align="left"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'left' );
-			} );
-
-			it( 'should upcast align=center attribute', () => {
-				editor.setData( '<table align="center"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.getAttribute( 'alignment' ) ).to.equal( 'center' );
-			} );
-
-			it( 'should discard align=justify attribute', () => {
-				editor.setData( '<table align="justify"><tr><td>foo</td></tr></table>' );
-				const table = model.document.getRoot().getNodeByPath( [ 0 ] );
-
-				expect( table.hasAttribute( 'alignment' ) ).to.be.false;
-			} );
-		} );
-
-		describe( 'downcast conversion', () => {
-			let table;
-
-			beforeEach( () => {
-				table = createEmptyTable();
-			} );
-
-			it( 'should downcast right alignment', () => {
-				model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
-
-				assertTableStyle( editor, 'margin-right:0;margin-left:auto;' );
-			} );
-
-			it( 'should downcast left alignment', () => {
-				model.change( writer => writer.setAttribute( 'alignment', 'left', table ) );
-
-				assertTableStyle( editor, 'margin-right:auto;margin-left:0;' );
-			} );
-
-			it( 'should downcast centered alignment', () => {
-				model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
-
-				assertTableStyle( editor, 'margin-right:auto;margin-left:auto;' );
-			} );
-
-			it( 'should downcast changed alignment', () => {
-				model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
-
-				assertTableStyle( editor, 'margin-right:auto;margin-left:auto;' );
-
-				model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
-
-				assertTableStyle( editor, 'margin-right:0;margin-left:auto;' );
-			} );
-
-			it( 'should downcast removed alignment', () => {
-				model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
-
-				assertTableStyle( editor, 'margin-right:auto;margin-left:auto;' );
-
-				model.change( writer => writer.removeAttribute( 'alignment', table ) );
-
-				assertTableStyle( editor );
-			} );
-		} );
-	} );
-
-	function createEmptyTable() {
-		setModelData(
-			model,
-			'<table headingRows="0" headingColumns="0">' +
-				'<tableRow>' +
-					'<tableCell>' +
-						'<paragraph>foo</paragraph>' +
-					'</tableCell>' +
-				'</tableRow>' +
-			'</table>'
-		);
-
-		return model.document.getRoot().getNodeByPath( [ 0 ] );
-	}
 } );

+ 153 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tablealignmentcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableStyle, modelTable } from '../../_utils/utils';
+import TablePropertiesEditing from '../../../src/tableproperties/tablepropertiesediting';
+import TableHorizontalAlignmentCommand from '../../../src/tableproperties/commands/tablealignmentcommand';
+
+describe( 'table properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableHorizontalAlignmentCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TablePropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableHorizontalAlignmentCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table has no alignment property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table has alignment property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { alignment: 'center' } ) );
+
+						expect( command.value ).to.equal( 'center' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ], { alignment: 'center' } ) );
+
+						expect( command.value ).to.equal( 'center' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: 'right', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table alignment to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+					} );
+
+					it( 'should change selected table alignment to a passed value', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { alignment: 'center' } ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+					} );
+
+					it( 'should remove alignment from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { alignment: 'center' } ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table alignment to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+					} );
+
+					it( 'should change selected table alignment to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'right' } );
+
+						assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+					} );
+
+					it( 'should remove alignment from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tablebackgroundcolorcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableStyle, modelTable } from '../../_utils/utils';
+import TablePropertiesEditing from '../../../src/tableproperties/tablepropertiesediting';
+import TableBackgroundColorCommand from '../../../src/tableproperties/commands/tablebackgroundcolorcommand';
+
+describe( 'table properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableBackgroundColorCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TablePropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableBackgroundColorCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table has no backgroundColor property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table has backgroundColor property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { backgroundColor: 'blue' } ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ], { backgroundColor: 'blue' } ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '#f00', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should change selected table backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { backgroundColor: 'blue' } ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should remove backgroundColor from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { backgroundColor: 'blue' } ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should change selected table backgroundColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'background-color:#f00;' );
+					} );
+
+					it( 'should remove backgroundColor from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 178 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tablebordercolorcommand.js

@@ -0,0 +1,178 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableStyle, modelTable, setTableWithObjectAttributes } from '../../_utils/utils';
+import TablePropertiesEditing from '../../../src/tableproperties/tablepropertiesediting';
+import TableBorderColorCommand from '../../../src/tableproperties/commands/tablebordercolorcommand';
+
+describe( 'table properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableBorderColorCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TablePropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableBorderColorCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table has no borderColor property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table has borderColor property (single string)', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderColor: 'blue' } ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+
+					it( 'should be set if selected table has borderColor property object with same values', () => {
+						setTableWithObjectAttributes( model, {
+							borderColor: {
+								top: 'blue',
+								right: 'blue',
+								bottom: 'blue',
+								left: 'blue'
+							}
+						}, '[]foo' );
+						expect( command.value ).to.equal( 'blue' );
+					} );
+
+					it( 'should be undefined if selected table has borderColor property object with different values', () => {
+						setTableWithObjectAttributes( model, {
+							borderColor: {
+								top: 'blue',
+								right: 'red',
+								bottom: 'blue',
+								left: 'blue'
+							}
+						}, '[]foo' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ], { borderColor: 'blue' } ) );
+
+						expect( command.value ).to.equal( 'blue' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '#f00', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should change selected table borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderColor: 'blue' } ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should remove borderColor from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderColor: 'blue' } ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should change selected table borderColor to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '#f00' } );
+
+						assertTableStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+					} );
+
+					it( 'should remove borderColor from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 178 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tableborderstylecommand.js

@@ -0,0 +1,178 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableStyle, modelTable, setTableWithObjectAttributes } from '../../_utils/utils';
+import TablePropertiesEditing from '../../../src/tableproperties/tablepropertiesediting';
+import TableBorderStyleCommand from '../../../src/tableproperties/commands/tableborderstylecommand';
+
+describe( 'table properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableBorderStyleCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TablePropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableBorderStyleCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table has no borderStyle property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table has borderStyle property (single string)', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderStyle: 'ridge' } ) );
+
+						expect( command.value ).to.equal( 'ridge' );
+					} );
+
+					it( 'should be set if selected table has borderStyle property object with same values', () => {
+						setTableWithObjectAttributes( model, {
+							borderStyle: {
+								top: 'ridge',
+								right: 'ridge',
+								bottom: 'ridge',
+								left: 'ridge'
+							}
+						}, '[]foo' );
+						expect( command.value ).to.equal( 'ridge' );
+					} );
+
+					it( 'should be undefined if selected table has borderStyle property object with different values', () => {
+						setTableWithObjectAttributes( model, {
+							borderStyle: {
+								top: 'ridge',
+								right: 'dashed',
+								bottom: 'ridge',
+								left: 'ridge'
+							}
+						}, '[]foo' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ], { borderStyle: 'ridge' } ) );
+
+						expect( command.value ).to.equal( 'ridge' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: 'solid', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should change selected table borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderStyle: 'ridge' } ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should remove borderStyle from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderStyle: 'ridge' } ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should change selected table borderStyle to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: 'solid' } );
+
+						assertTableStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+					} );
+
+					it( 'should remove borderStyle from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 178 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tableborderwidthcommand.js

@@ -0,0 +1,178 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableStyle, modelTable, setTableWithObjectAttributes } from '../../_utils/utils';
+import TablePropertiesEditing from '../../../src/tableproperties/tablepropertiesediting';
+import TableBorderWidthCommand from '../../../src/tableproperties/commands/tableborderwidthcommand';
+
+describe( 'table properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableBorderWidthCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TablePropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableBorderWidthCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table has no borderWidth property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table has borderWidth property (single string)', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderWidth: '2em' } ) );
+
+						expect( command.value ).to.equal( '2em' );
+					} );
+
+					it( 'should be set if selected table has borderWidth property object with same values', () => {
+						setTableWithObjectAttributes( model, {
+							borderWidth: {
+								top: '2em',
+								right: '2em',
+								bottom: '2em',
+								left: '2em'
+							}
+						}, '[]foo' );
+						expect( command.value ).to.equal( '2em' );
+					} );
+
+					it( 'should be undefined if selected table has borderWidth property object with different values', () => {
+						setTableWithObjectAttributes( model, {
+							borderWidth: {
+								top: '2em',
+								right: '333px',
+								bottom: '2em',
+								left: '2em'
+							}
+						}, '[]foo' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ], { borderWidth: '2em' } ) );
+
+						expect( command.value ).to.equal( '2em' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '1px', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should change selected table borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderWidth: '2em' } ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should remove borderWidth from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { borderWidth: '2em' } ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should change selected table borderWidth to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '1px' } );
+
+						assertTableStyle( editor, 'border-bottom:1px;border-left:1px;border-right:1px;border-top:1px;' );
+					} );
+
+					it( 'should remove borderWidth from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tableheightcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableStyle, modelTable } from '../../_utils/utils';
+import TablePropertiesEditing from '../../../src/tableproperties/tablepropertiesediting';
+import TableHeightCommand from '../../../src/tableproperties/commands/tableheightcommand';
+
+describe( 'table properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableHeightCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TablePropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableHeightCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table has no height property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table has height property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { height: '100px' } ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ], { height: '100px' } ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '25px', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table height to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should change selected table height to a passed value', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { height: '100px' } ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should remove height from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ { height: '100px', contents: '[]foo' } ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table height to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should change selected table height to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'height:25px;' );
+					} );
+
+					it( 'should remove height from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 153 - 0
packages/ckeditor5-table/tests/tableproperties/commands/tablewidthcommand.js

@@ -0,0 +1,153 @@
+/**
+ * @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 ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import { assertTableStyle, modelTable } from '../../_utils/utils';
+import TablePropertiesEditing from '../../../src/tableproperties/tablepropertiesediting';
+import TableWidthCommand from '../../../src/tableproperties/commands/tablewidthcommand';
+
+describe( 'table properties', () => {
+	describe( 'commands', () => {
+		describe( 'TableWidthCommand', () => {
+			let editor, model, command;
+
+			beforeEach( async () => {
+				editor = await ModelTestEditor.create( {
+					plugins: [ Paragraph, TablePropertiesEditing ]
+				} );
+
+				model = editor.model;
+				command = new TableWidthCommand( editor );
+			} );
+
+			afterEach( () => {
+				return editor.destroy();
+			} );
+
+			describe( 'isEnabled', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>foo[]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+						expect( command.isEnabled ).to.be.false;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ] ) );
+						expect( command.isEnabled ).to.be.true;
+					} );
+				} );
+			} );
+
+			describe( 'value', () => {
+				describe( 'collapsed selection', () => {
+					it( 'should be undefined if selected table has no width property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ] ) );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be set if selected table has width property', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { width: '100px' } ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should be false if selection does not have table', () => {
+						setData( model, '<paragraph>f[oo]</paragraph>' );
+
+						expect( command.value ).to.be.undefined;
+					} );
+
+					it( 'should be true is selection has table', () => {
+						setData( model, modelTable( [ [ 'f[o]o' ] ], { width: '100px' } ) );
+
+						expect( command.value ).to.equal( '100px' );
+					} );
+				} );
+			} );
+
+			describe( 'execute()', () => {
+				it( 'should use provided batch', () => {
+					setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+					const batch = model.createBatch();
+					const spy = sinon.spy( model, 'enqueueChange' );
+
+					command.execute( { value: '25px', batch } );
+					sinon.assert.calledWith( spy, batch );
+				} );
+
+				describe( 'collapsed selection', () => {
+					it( 'should set selected table width to a passed value', () => {
+						setData( model, modelTable( [ [ 'foo[]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should change selected table width to a passed value', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { width: '100px' } ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should remove width from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[]foo' ] ], { width: '100px' } ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+
+				describe( 'non-collapsed selection', () => {
+					it( 'should set selected table width to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should change selected table width to a passed value', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute( { value: '25px' } );
+
+						assertTableStyle( editor, 'width:25px;' );
+					} );
+
+					it( 'should remove width from a selected table if no value is passed', () => {
+						setData( model, modelTable( [ [ '[foo]' ] ] ) );
+
+						command.execute();
+
+						assertTableStyle( editor, '' );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );

+ 716 - 0
packages/ckeditor5-table/tests/tableproperties/tablepropertiesediting.js

@@ -0,0 +1,716 @@
+/**
+ * @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 Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import TableEditing from '../../src/tableediting';
+import TableProperties from '../../src/tableproperties';
+
+import TableBorderColorCommand from '../../src/tableproperties/commands/tablebordercolorcommand';
+import TableBorderStyleCommand from '../../src/tableproperties/commands/tableborderstylecommand';
+import TableBorderWidthCommand from '../../src/tableproperties/commands/tableborderwidthcommand';
+import TableAlignmentCommand from '../../src/tableproperties/commands/tablealignmentcommand';
+import TableWidthCommand from '../../src/tableproperties/commands/tablewidthcommand';
+import TableHeightCommand from '../../src/tableproperties/commands/tableheightcommand';
+import TableBackgroundColorCommand from '../../src/tableproperties/commands/tablebackgroundcolorcommand';
+
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { assertTableStyle, assertTRBLAttribute } from '../_utils/utils';
+
+describe( 'table properties', () => {
+	describe( 'TablePropertiesEditing', () => {
+		let editor, model;
+
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ TableProperties, Paragraph, TableEditing ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					model = editor.model;
+				} );
+		} );
+
+		afterEach( () => {
+			editor.destroy();
+		} );
+
+		it( 'should have pluginName', () => {
+			expect( TableProperties.pluginName ).to.equal( 'TableProperties' );
+		} );
+
+		it( 'adds tableBorderColor command', () => {
+			expect( editor.commands.get( 'tableBorderColor' ) ).to.be.instanceOf( TableBorderColorCommand );
+		} );
+
+		it( 'adds tableBorderStyle command', () => {
+			expect( editor.commands.get( 'tableBorderStyle' ) ).to.be.instanceOf( TableBorderStyleCommand );
+		} );
+
+		it( 'adds tableBorderWidth command', () => {
+			expect( editor.commands.get( 'tableBorderWidth' ) ).to.be.instanceOf( TableBorderWidthCommand );
+		} );
+
+		it( 'adds tableAlignment command', () => {
+			expect( editor.commands.get( 'tableAlignment' ) ).to.be.instanceOf( TableAlignmentCommand );
+		} );
+
+		it( 'adds tableWidth command', () => {
+			expect( editor.commands.get( 'tableWidth' ) ).to.be.instanceOf( TableWidthCommand );
+		} );
+
+		it( 'adds tableHeight command', () => {
+			expect( editor.commands.get( 'tableHeight' ) ).to.be.instanceOf( TableHeightCommand );
+		} );
+
+		it( 'adds tableBackgroundColor command', () => {
+			expect( editor.commands.get( 'tableBackgroundColor' ) ).to.be.instanceOf( TableBackgroundColorCommand );
+		} );
+
+		describe( 'border', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'borderColor' ) ).to.be.true;
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'borderStyle' ) ).to.be.true;
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'borderWidth' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast border shorthand', () => {
+					editor.setData( '<table style="border:1px solid #f00"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', '#f00' );
+					assertTRBLAttribute( table, 'borderStyle', 'solid' );
+					assertTRBLAttribute( table, 'borderWidth', '1px' );
+				} );
+
+				it( 'should upcast border-color shorthand', () => {
+					editor.setData( '<table style="border-color:#f00"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', '#f00' );
+				} );
+
+				it( 'should upcast border-style shorthand', () => {
+					editor.setData( '<table style="border-style:ridge"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderStyle', 'ridge' );
+				} );
+
+				it( 'should upcast border-width shorthand', () => {
+					editor.setData( '<table style="border-width:1px"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderWidth', '1px' );
+				} );
+
+				it( 'should upcast border-top shorthand', () => {
+					editor.setData( '<table style="border-top:1px solid #f00"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', '#f00', null, null, null );
+					assertTRBLAttribute( table, 'borderStyle', 'solid', null, null, null );
+					assertTRBLAttribute( table, 'borderWidth', '1px', null, null, null );
+				} );
+
+				it( 'should upcast border-right shorthand', () => {
+					editor.setData( '<table style="border-right:1px solid #f00"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', null, '#f00', null, null );
+					assertTRBLAttribute( table, 'borderStyle', null, 'solid', null, null );
+					assertTRBLAttribute( table, 'borderWidth', null, '1px', null, null );
+				} );
+
+				it( 'should upcast border-bottom shorthand', () => {
+					editor.setData( '<table style="border-bottom:1px solid #f00"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', null, null, '#f00', null );
+					assertTRBLAttribute( table, 'borderStyle', null, null, 'solid', null );
+					assertTRBLAttribute( table, 'borderWidth', null, null, '1px', null );
+				} );
+
+				it( 'should upcast border-left shorthand', () => {
+					editor.setData( '<table style="border-left:1px solid #f00"><tr><td>foo</td></tr></table>' );
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', null, null, null, '#f00' );
+					assertTRBLAttribute( table, 'borderStyle', null, null, null, 'solid' );
+					assertTRBLAttribute( table, 'borderWidth', null, null, null, '1px' );
+				} );
+
+				it( 'should upcast border-top-* styles', () => {
+					editor.setData(
+						'<table style="border-top-width:1px;border-top-style:solid;border-top-color:#f00"><tr><td>foo</td></tr></table>'
+					);
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', '#f00', null, null, null );
+					assertTRBLAttribute( table, 'borderStyle', 'solid', null, null, null );
+					assertTRBLAttribute( table, 'borderWidth', '1px', null, null, null );
+				} );
+
+				it( 'should upcast border-right-* styles', () => {
+					editor.setData(
+						'<table style="border-right-width:1px;border-right-style:solid;border-right-color:#f00">' +
+							'<tr>' +
+								'<td>foo</td>' +
+							'</tr>' +
+						'</table>'
+					);
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', null, '#f00', null, null );
+					assertTRBLAttribute( table, 'borderStyle', null, 'solid', null, null );
+					assertTRBLAttribute( table, 'borderWidth', null, '1px', null, null );
+				} );
+
+				it( 'should upcast border-bottom-* styles', () => {
+					editor.setData(
+						'<table style="border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#f00">' +
+						'<tr>' +
+						'<td>foo</td>' +
+						'</tr>' +
+						'</table>'
+					);
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', null, null, '#f00', null );
+					assertTRBLAttribute( table, 'borderStyle', null, null, 'solid', null );
+					assertTRBLAttribute( table, 'borderWidth', null, null, '1px', null );
+				} );
+
+				it( 'should upcast border-left-* styles', () => {
+					editor.setData(
+						'<table style="border-left-width:1px;border-left-style:solid;border-left-color:#f00"><tr><td>foo</td></tr></table>'
+					);
+
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					assertTRBLAttribute( table, 'borderColor', null, null, null, '#f00' );
+					assertTRBLAttribute( table, 'borderStyle', null, null, null, 'solid' );
+					assertTRBLAttribute( table, 'borderWidth', null, null, null, '1px' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let table;
+
+				beforeEach( () => {
+					table = createEmptyTable();
+				} );
+
+				it( 'should downcast borderColor attribute (same top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderColor', {
+						top: '#f00',
+						right: '#f00',
+						bottom: '#f00',
+						left: '#f00'
+					}, table ) );
+
+					assertTableStyle( editor, 'border-bottom:#f00;border-left:#f00;border-right:#f00;border-top:#f00;' );
+				} );
+
+				it( 'should downcast borderColor attribute (different top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderColor', {
+						top: '#f00',
+						right: 'hsla(0, 100%, 50%, 0.5)',
+						bottom: 'deeppink',
+						left: 'rgb(255, 0, 0)'
+					}, table ) );
+
+					assertTableStyle( editor,
+						'border-bottom:deeppink;' +
+						'border-left:rgb(255, 0, 0);' +
+						'border-right:hsla(0, 100%, 50%, 0.5);' +
+						'border-top:#f00;'
+					);
+				} );
+
+				it( 'should downcast borderStyle attribute (same top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderStyle', {
+						top: 'solid',
+						right: 'solid',
+						bottom: 'solid',
+						left: 'solid'
+					}, table ) );
+
+					assertTableStyle( editor, 'border-bottom:solid;border-left:solid;border-right:solid;border-top:solid;' );
+				} );
+
+				it( 'should downcast borderStyle attribute (different top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderStyle', {
+						top: 'solid',
+						right: 'ridge',
+						bottom: 'dotted',
+						left: 'dashed'
+					}, table ) );
+
+					assertTableStyle( editor, 'border-bottom:dotted;border-left:dashed;border-right:ridge;border-top:solid;' );
+				} );
+
+				it( 'should downcast borderWidth attribute (same top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderWidth', {
+						top: '42px',
+						right: '.1em',
+						bottom: '1337rem',
+						left: 'thick'
+					}, table ) );
+
+					assertTableStyle( editor, 'border-bottom:1337rem;border-left:thick;border-right:.1em;border-top:42px;' );
+				} );
+
+				it( 'should downcast borderWidth attribute (different top, right, bottom, left)', () => {
+					model.change( writer => writer.setAttribute( 'borderWidth', {
+						top: '42px',
+						right: '42px',
+						bottom: '42px',
+						left: '42px'
+					}, table ) );
+
+					assertTableStyle( editor, 'border-bottom:42px;border-left:42px;border-right:42px;border-top:42px;' );
+				} );
+
+				it( 'should downcast borderColor, borderStyle and borderWidth attributes together (same top, right, bottom, left)', () => {
+					model.change( writer => {
+						writer.setAttribute( 'borderColor', {
+							top: '#f00',
+							right: '#f00',
+							bottom: '#f00',
+							left: '#f00'
+						}, table );
+
+						writer.setAttribute( 'borderStyle', {
+							top: 'solid',
+							right: 'solid',
+							bottom: 'solid',
+							left: 'solid'
+						}, table );
+
+						writer.setAttribute( 'borderWidth', {
+							top: '42px',
+							right: '42px',
+							bottom: '42px',
+							left: '42px'
+						}, table );
+					} );
+
+					assertTableStyle( editor,
+						'border-bottom:42px solid #f00;' +
+						'border-left:42px solid #f00;' +
+						'border-right:42px solid #f00;' +
+						'border-top:42px solid #f00;'
+					);
+				} );
+
+				it(
+					'should downcast borderColor, borderStyle and borderWidth attributes together (different top, right, bottom, left)',
+					() => {
+						model.change( writer => {
+							writer.setAttribute( 'borderColor', {
+								top: '#f00',
+								right: 'hsla(0, 100%, 50%, 0.5)',
+								bottom: 'deeppink',
+								left: 'rgb(255, 0, 0)'
+							}, table );
+
+							writer.setAttribute( 'borderStyle', {
+								top: 'solid',
+								right: 'ridge',
+								bottom: 'dotted',
+								left: 'dashed'
+							}, table );
+
+							writer.setAttribute( 'borderWidth', {
+								top: '42px',
+								right: '.1em',
+								bottom: '1337rem',
+								left: 'thick'
+							}, table );
+						} );
+
+						assertTableStyle( editor,
+							'border-bottom:1337rem dotted deeppink;' +
+							'border-left:thick dashed rgb(255, 0, 0);' +
+							'border-right:.1em ridge hsla(0, 100%, 50%, 0.5);' +
+							'border-top:42px solid #f00;'
+						);
+					}
+				);
+
+				describe( 'change attribute', () => {
+					beforeEach( () => {
+						model.change( writer => {
+							writer.setAttribute( 'borderColor', {
+								top: '#f00',
+								right: '#f00',
+								bottom: '#f00',
+								left: '#f00'
+							}, table );
+
+							writer.setAttribute( 'borderStyle', {
+								top: 'solid',
+								right: 'solid',
+								bottom: 'solid',
+								left: 'solid'
+							}, table );
+
+							writer.setAttribute( 'borderWidth', {
+								top: '42px',
+								right: '42px',
+								bottom: '42px',
+								left: '42px'
+							}, table );
+						} );
+					} );
+
+					it( 'should downcast borderColor attribute change', () => {
+						model.change( writer => writer.setAttribute( 'borderColor', {
+							top: 'deeppink',
+							right: 'deeppink',
+							bottom: 'deeppink',
+							left: 'deeppink'
+						}, table ) );
+
+						assertTableStyle( editor,
+							'border-bottom:42px solid deeppink;' +
+							'border-left:42px solid deeppink;' +
+							'border-right:42px solid deeppink;' +
+							'border-top:42px solid deeppink;'
+						);
+					} );
+
+					it( 'should downcast borderStyle attribute change', () => {
+						model.change( writer => writer.setAttribute( 'borderStyle', {
+							top: 'ridge',
+							right: 'ridge',
+							bottom: 'ridge',
+							left: 'ridge'
+						}, table ) );
+
+						assertTableStyle( editor,
+							'border-bottom:42px ridge #f00;' +
+							'border-left:42px ridge #f00;' +
+							'border-right:42px ridge #f00;' +
+							'border-top:42px ridge #f00;'
+						);
+					} );
+
+					it( 'should downcast borderWidth attribute change', () => {
+						model.change( writer => writer.setAttribute( 'borderWidth', {
+							top: 'thick',
+							right: 'thick',
+							bottom: 'thick',
+							left: 'thick'
+						}, table ) );
+
+						assertTableStyle( editor,
+							'border-bottom:thick solid #f00;' +
+							'border-left:thick solid #f00;' +
+							'border-right:thick solid #f00;' +
+							'border-top:thick solid #f00;'
+						);
+					} );
+
+					it( 'should downcast borderColor attribute removal', () => {
+						model.change( writer => writer.removeAttribute( 'borderColor', table ) );
+
+						assertTableStyle( editor,
+							'border-bottom:42px solid;' +
+							'border-left:42px solid;' +
+							'border-right:42px solid;' +
+							'border-top:42px solid;'
+						);
+					} );
+
+					it( 'should downcast borderStyle attribute removal', () => {
+						model.change( writer => writer.removeAttribute( 'borderStyle', table ) );
+
+						assertTableStyle( editor,
+							'border-bottom:42px #f00;' +
+							'border-left:42px #f00;' +
+							'border-right:42px #f00;' +
+							'border-top:42px #f00;'
+						);
+					} );
+
+					it( 'should downcast borderWidth attribute removal', () => {
+						model.change( writer => writer.removeAttribute( 'borderWidth', table ) );
+
+						assertTableStyle( editor,
+							'border-bottom:solid #f00;' +
+							'border-left:solid #f00;' +
+							'border-right:solid #f00;' +
+							'border-top:solid #f00;'
+						);
+					} );
+
+					it( 'should downcast borderColor, borderStyle and borderWidth attributes removal', () => {
+						model.change( writer => {
+							writer.removeAttribute( 'borderColor', table );
+							writer.removeAttribute( 'borderStyle', table );
+							writer.removeAttribute( 'borderWidth', table );
+						} );
+
+						assertEqualMarkup(
+							editor.getData(),
+							'<figure class="table"><table><tbody><tr><td>foo</td></tr></tbody></table></figure>'
+						);
+					} );
+				} );
+			} );
+		} );
+
+		describe( 'background color', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'backgroundColor' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast background-color', () => {
+					editor.setData( '<table style="background-color:#f00"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
+				} );
+
+				it( 'should upcast from background shorthand', () => {
+					editor.setData( '<table style="background:#f00 center center"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'backgroundColor' ) ).to.equal( '#f00' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let table;
+
+				beforeEach( () => {
+					table = createEmptyTable();
+				} );
+
+				it( 'should downcast backgroundColor', () => {
+					model.change( writer => writer.setAttribute( 'backgroundColor', '#f00', table ) );
+
+					assertTableStyle( editor, 'background-color:#f00;' );
+				} );
+			} );
+		} );
+
+		describe( 'width', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'width' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast width', () => {
+					editor.setData( '<table style="width:1337px"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'width' ) ).to.equal( '1337px' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let table;
+
+				beforeEach( () => {
+					table = createEmptyTable();
+				} );
+
+				it( 'should downcast width', () => {
+					model.change( writer => writer.setAttribute( 'width', '1337px', table ) );
+
+					assertTableStyle( editor, 'width:1337px;' );
+				} );
+			} );
+		} );
+
+		describe( 'height', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'height' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast height', () => {
+					editor.setData( '<table style="height:1337px"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'height' ) ).to.equal( '1337px' );
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let table;
+
+				beforeEach( () => {
+					table = createEmptyTable();
+				} );
+
+				it( 'should downcast height', () => {
+					model.change( writer => writer.setAttribute( 'height', '1337px', table ) );
+
+					assertTableStyle( editor, 'height:1337px;' );
+				} );
+			} );
+		} );
+
+		describe( 'alignment', () => {
+			it( 'should set proper schema rules', () => {
+				expect( model.schema.checkAttribute( [ '$root', 'table' ], 'alignment' ) ).to.be.true;
+			} );
+
+			describe( 'upcast conversion', () => {
+				it( 'should upcast style="margin-left:auto;margin-right:0" to right value', () => {
+					editor.setData( '<table style="margin-left:auto;margin-right:0"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
+				} );
+
+				it( 'should upcast style="margin-left:0;margin-right:auto" to left value', () => {
+					editor.setData( '<table style="margin-left:0;margin-right:auto"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'left' );
+				} );
+
+				it( 'should upcast style="margin-left:auto;margin-right:auto" to center value', () => {
+					editor.setData( '<table style="margin-left:auto;margin-right:auto"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'center' );
+				} );
+
+				it( 'should upcast style="margin-left:auto;margin-right:0pt" to right value', () => {
+					editor.setData( '<table style="margin-left:auto;margin-right:0pt"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
+				} );
+
+				it( 'should upcast style="margin-left:auto;margin-right:0%" to right value', () => {
+					editor.setData( '<table style="margin-left:auto;margin-right:0%"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
+				} );
+
+				it( 'should not upcast style="margin-left:auto;margin-right:0.23pt" to right value', () => {
+					editor.setData( '<table style="margin-left:auto;margin-right:0.23pt"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.hasAttribute( 'alignment' ) ).to.be.false;
+				} );
+
+				it( 'should upcast align=right attribute', () => {
+					editor.setData( '<table align="right"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'right' );
+				} );
+
+				it( 'should upcast align=left attribute', () => {
+					editor.setData( '<table align="left"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'left' );
+				} );
+
+				it( 'should upcast align=center attribute', () => {
+					editor.setData( '<table align="center"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.getAttribute( 'alignment' ) ).to.equal( 'center' );
+				} );
+
+				it( 'should discard align=justify attribute', () => {
+					editor.setData( '<table align="justify"><tr><td>foo</td></tr></table>' );
+					const table = model.document.getRoot().getNodeByPath( [ 0 ] );
+
+					expect( table.hasAttribute( 'alignment' ) ).to.be.false;
+				} );
+			} );
+
+			describe( 'downcast conversion', () => {
+				let table;
+
+				beforeEach( () => {
+					table = createEmptyTable();
+				} );
+
+				it( 'should downcast right alignment', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
+
+					assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+				} );
+
+				it( 'should downcast left alignment', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'left', table ) );
+
+					assertTableStyle( editor, 'margin-left:0;margin-right:auto;' );
+				} );
+
+				it( 'should downcast centered alignment', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
+
+					assertTableStyle( editor, 'margin-left:auto;margin-right:auto;' );
+				} );
+
+				it( 'should downcast changed alignment', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
+
+					assertTableStyle( editor, 'margin-left:auto;margin-right:auto;' );
+
+					model.change( writer => writer.setAttribute( 'alignment', 'right', table ) );
+
+					assertTableStyle( editor, 'margin-left:auto;margin-right:0;' );
+				} );
+
+				it( 'should downcast removed alignment', () => {
+					model.change( writer => writer.setAttribute( 'alignment', 'center', table ) );
+
+					assertTableStyle( editor, 'margin-left:auto;margin-right:auto;' );
+
+					model.change( writer => writer.removeAttribute( 'alignment', table ) );
+
+					assertTableStyle( editor );
+				} );
+			} );
+		} );
+
+		function createEmptyTable() {
+			setModelData(
+				model,
+				'<table headingRows="0" headingColumns="0">' +
+				'<tableRow>' +
+				'<tableCell>' +
+				'<paragraph>foo</paragraph>' +
+				'</tableCell>' +
+				'</tableRow>' +
+				'</table>'
+			);
+
+			return model.document.getRoot().getNodeByPath( [ 0 ] );
+		}
+	} );
+} );