浏览代码

Merge pull request #23 from ckeditor/t/1

Feature: Implemented the basic user interface of the feature (see #1).
Aleksander Nowodzinski 7 年之前
父节点
当前提交
6629d1acec
共有 30 个文件被更改,包括 1936 次插入231 次删除
  1. 5 1
      packages/ckeditor5-table/src/commands/insertcolumncommand.js
  2. 5 1
      packages/ckeditor5-table/src/commands/insertrowcommand.js
  3. 8 1
      packages/ckeditor5-table/src/commands/inserttablecommand.js
  4. 5 1
      packages/ckeditor5-table/src/commands/mergecellcommand.js
  5. 1 1
      packages/ckeditor5-table/src/commands/removecolumncommand.js
  6. 96 0
      packages/ckeditor5-table/src/commands/setheadercolumncommand.js
  7. 55 27
      packages/ckeditor5-table/src/commands/settableheaderscommand.js
  8. 3 2
      packages/ckeditor5-table/src/converters/downcast.js
  9. 15 0
      packages/ckeditor5-table/src/table.js
  10. 5 3
      packages/ckeditor5-table/src/tableediting.js
  11. 192 0
      packages/ckeditor5-table/src/tabletoolbar.js
  12. 143 41
      packages/ckeditor5-table/src/tableui.js
  13. 183 0
      packages/ckeditor5-table/src/ui/inserttableview.js
  14. 51 0
      packages/ckeditor5-table/src/ui/utils.js
  15. 51 0
      packages/ckeditor5-table/src/utils.js
  16. 191 0
      packages/ckeditor5-table/tests/commands/setheadercolumncommand.js
  17. 103 87
      packages/ckeditor5-table/tests/commands/settableheaderscommand.js
  18. 86 0
      packages/ckeditor5-table/tests/integration.js
  19. 7 8
      packages/ckeditor5-table/tests/manual/table.html
  20. 7 4
      packages/ckeditor5-table/tests/manual/table.js
  21. 64 0
      packages/ckeditor5-table/tests/manual/table.md
  22. 8 3
      packages/ckeditor5-table/tests/tableediting.js
  23. 192 0
      packages/ckeditor5-table/tests/tabletoolbar.js
  24. 289 51
      packages/ckeditor5-table/tests/tableui.js
  25. 126 0
      packages/ckeditor5-table/tests/ui/inserttableview.js
  26. 1 0
      packages/ckeditor5-table/theme/icons/table-column.svg
  27. 1 0
      packages/ckeditor5-table/theme/icons/table-merge-cell.svg
  28. 1 0
      packages/ckeditor5-table/theme/icons/table-row.svg
  29. 1 0
      packages/ckeditor5-table/theme/icons/table.svg
  30. 41 0
      packages/ckeditor5-table/theme/inserttable.css

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

@@ -49,7 +49,11 @@ export default class InsertColumnCommand extends Command {
 	}
 
 	/**
-	 * @inheritDoc
+	 * Executes the command.
+	 *
+	 * Depending on command's {@link #order} value it inserts a column `'before'` or `'after'` the column in which selection is set.
+	 *
+	 * @fires execute
 	 */
 	execute() {
 		const editor = this.editor;

+ 5 - 1
packages/ckeditor5-table/src/commands/insertrowcommand.js

@@ -49,7 +49,11 @@ export default class InsertRowCommand extends Command {
 	}
 
 	/**
-	 * @inheritDoc
+	 * Executes the command.
+	 *
+	 * Depending on command's {@link #order} value it inserts a row `'below'` or `'above'` the row in which selection is set.
+	 *
+	 * @fires execute
 	 */
 	execute() {
 		const editor = this.editor;

+ 8 - 1
packages/ckeditor5-table/src/commands/inserttablecommand.js

@@ -31,7 +31,14 @@ export default class InsertTableCommand extends Command {
 	}
 
 	/**
-	 * @inheritDoc
+	 * Executes the command.
+	 *
+	 * Inserts table of given rows and columns into the editor.
+	 *
+	 * @param {Object} options
+	 * @param {Number} [options.rows=2] Number of rows to create in inserted table.
+	 * @param {Number} [options.columns=2] Number of columns to create in inserted table.
+	 * @fires execute
 	 */
 	execute( options = {} ) {
 		const model = this.editor.model;

+ 5 - 1
packages/ckeditor5-table/src/commands/mergecellcommand.js

@@ -59,7 +59,11 @@ export default class MergeCellCommand extends Command {
 	}
 
 	/**
-	 * @inheritDoc
+	 * Executes the command.
+	 *
+	 * Depending on command's {@link #direction} value it will merge a cell that is to the `'left'`, `'right'`, `'up'` or `'down'`.
+	 *
+	 * @fires execute
 	 */
 	execute() {
 		const model = this.editor.model;

+ 1 - 1
packages/ckeditor5-table/src/commands/removecolumncommand.js

@@ -14,7 +14,7 @@ import TableUtils from '../tableutils';
 import { updateNumericAttribute } from './utils';
 
 /**
- * The split cell command.
+ * The remove column command.
  *
  * @extends module:core/command~Command
  */

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

@@ -0,0 +1,96 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/commands/setheadercolumncommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+import { getParentTable, updateNumericAttribute } from './utils';
+
+/**
+ * The header coloumn command.
+ *
+ * @extends module:core/command~Command
+ */
+export default class SetHeaderColumnCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const model = this.editor.model;
+		const doc = model.document;
+		const selection = doc.selection;
+
+		const position = selection.getFirstPosition();
+		const tableParent = getParentTable( position );
+
+		const isInTable = !!tableParent;
+
+		this.isEnabled = isInTable;
+
+		/**
+		 * Flag indicating whether the command is active. The command is active when the
+		 * {@link module:engine/model/selection~Selection} is in a header column.
+		 *
+		 * @observable
+		 * @readonly
+		 * @member {Boolean} #value
+		 */
+		this.value = isInTable && this._isInHeading( position.parent, tableParent );
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 * When the selection is non-header column, the command will set `headingColumns` table's attribute to cover that column.
+	 *
+	 * When selection is already in a header column then it will set `headingColumns` so the heading section will end before that column.
+	 *
+	 * @fires execute
+	 */
+	execute() {
+		const model = this.editor.model;
+		const doc = model.document;
+		const selection = doc.selection;
+		const tableUtils = this.editor.plugins.get( 'TableUtils' );
+
+		const position = selection.getFirstPosition();
+		const tableCell = position.parent;
+		const tableRow = tableCell.parent;
+		const table = tableRow.parent;
+
+		const currentHeadingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
+
+		let { column } = tableUtils.getCellLocation( tableCell );
+
+		if ( column + 1 !== currentHeadingColumns ) {
+			column++;
+		}
+
+		model.change( writer => {
+			updateNumericAttribute( 'headingColumns', column, table, writer, 0 );
+		} );
+	}
+
+	/**
+	 * Checks if a table cell is in the heading section.
+	 *
+	 * @param {module:engine/model/element~Element} tableCell
+	 * @param {module:engine/model/element~Element} table
+	 * @returns {Boolean}
+	 * @private
+	 */
+	_isInHeading( tableCell, table ) {
+		const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
+
+		const tableUtils = this.editor.plugins.get( 'TableUtils' );
+
+		const { column } = tableUtils.getCellLocation( tableCell );
+
+		return !!headingColumns && column < headingColumns;
+	}
+}

+ 55 - 27
packages/ckeditor5-table/src/commands/settableheaderscommand.js

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

+ 3 - 2
packages/ckeditor5-table/src/converters/downcast.js

@@ -10,7 +10,8 @@
 import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
 import TableWalker from './../tablewalker';
-import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
+import { toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
+import { toTableWidget } from '../utils';
 
 /**
  * Model table element to view table element conversion helper.
@@ -40,7 +41,7 @@ export function downcastInsertTable( options = {} ) {
 		let tableWidget;
 
 		if ( asWidget ) {
-			tableWidget = toWidget( tableElement, conversionApi.writer );
+			tableWidget = toTableWidget( tableElement, conversionApi.writer );
 		}
 
 		const tableWalker = new TableWalker( table );

+ 15 - 0
packages/ckeditor5-table/src/table.js

@@ -33,3 +33,18 @@ export default class Table extends Plugin {
 		return 'Table';
 	}
 }
+
+/**
+ * The configuration of the table features. Used by the table features in the `@ckeditor/ckeditor5-table` package.
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ * 				table: ... // Table feature options.
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @interface TableConfig
+ */

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

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

+ 192 - 0
packages/ckeditor5-table/src/tabletoolbar.js

@@ -0,0 +1,192 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/tabletoolbar
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
+import { isTableWidgetSelected } from './utils';
+import { repositionContextualBalloon, getBalloonPositionData } from './ui/utils';
+
+const balloonClassName = 'ck-toolbar-container';
+
+/**
+ * The table toolbar class. Creates a table toolbar that shows up when the table widget is selected.
+ *
+ * Toolbar components are created using the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}
+ * based on the {@link module:core/editor/editor~Editor#config configuration} stored under `table.toolbar`.
+ *
+ * The toolbar uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TableToolbar extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ContextualBalloon ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableToolbar';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
+
+		// If the `BalloonToolbar` plugin is loaded, it should be disabled for tables
+		// which have their own toolbar to avoid duplication.
+		// https://github.com/ckeditor/ckeditor5-image/issues/110
+		if ( balloonToolbar ) {
+			this.listenTo( balloonToolbar, 'show', evt => {
+				if ( isTableWidgetSelected( editor.editing.view.document.selection ) ) {
+					evt.stop();
+				}
+			}, { priority: 'high' } );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	afterInit() {
+		const editor = this.editor;
+		const toolbarConfig = editor.config.get( 'table.toolbar' );
+
+		// Don't add the toolbar if there is no configuration.
+		if ( !toolbarConfig || !toolbarConfig.length ) {
+			return;
+		}
+
+		/**
+		 * A contextual balloon plugin instance.
+		 *
+		 * @private
+		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
+		 */
+		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
+
+		/**
+		 * A `ToolbarView` instance used to display the buttons specific for table editing.
+		 *
+		 * @protected
+		 * @type {module:ui/toolbar/toolbarview~ToolbarView}
+		 */
+		this._toolbar = new ToolbarView();
+
+		// Add buttons to the toolbar.
+		this._toolbar.fillFromConfig( toolbarConfig, editor.ui.componentFactory );
+
+		// Show balloon panel each time table widget is selected.
+		this.listenTo( editor.editing.view, 'render', () => {
+			this._checkIsVisible();
+		} );
+
+		// There is no render method after focus is back in editor, we need to check if balloon panel should be visible.
+		this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
+			this._checkIsVisible();
+		}, { priority: 'low' } );
+	}
+
+	/**
+	 * Checks whether the toolbar should show up or hide depending on the current selection.
+	 *
+	 * @private
+	 */
+	_checkIsVisible() {
+		const editor = this.editor;
+
+		if ( !editor.ui.focusTracker.isFocused ) {
+			this._hideToolbar();
+		} else {
+			if ( isTableWidgetSelected( editor.editing.view.document.selection ) ) {
+				this._showToolbar();
+			} else {
+				this._hideToolbar();
+			}
+		}
+	}
+
+	/**
+	 * Shows the {@link #_toolbar} in the {@link #_balloon}.
+	 *
+	 * @private
+	 */
+	_showToolbar() {
+		const editor = this.editor;
+
+		if ( this._isVisible ) {
+			repositionContextualBalloon( editor );
+		} else {
+			if ( !this._balloon.hasView( this._toolbar ) ) {
+				this._balloon.add( {
+					view: this._toolbar,
+					position: getBalloonPositionData( editor ),
+					balloonClassName
+				} );
+			}
+		}
+	}
+
+	/**
+	 * Removes the {@link #_toolbar} from the {@link #_balloon}.
+	 *
+	 * @private
+	 */
+	_hideToolbar() {
+		if ( !this._isVisible ) {
+			return;
+		}
+
+		this._balloon.remove( this._toolbar );
+	}
+
+	/**
+	 * Returns `true` when the {@link #_toolbar} is the visible view in the {@link #_balloon}.
+	 *
+	 * @private
+	 * @type {Boolean}
+	 */
+	get _isVisible() {
+		return this._balloon.visibleView == this._toolbar;
+	}
+}
+
+/**
+ * Items to be placed in the table toolbar.
+ * This option is used by the {@link module:table/tabletoolbar~TableToolbar} feature.
+ *
+ * Assuming that you use the {@link module:table/tableui~TableUI} feature the following toolbar items will be available
+ * in {@link module:ui/componentfactory~ComponentFactory}:
+ *
+ * * `'tableRow'`,
+ * * `'tableColumn'`,
+ * * `'mergeCell'`,
+ * * `'splitCell'`,
+ *
+ * so you can configure the toolbar like this:
+ *
+ *		const tableConfig = {
+ *			toolbar: [ 'tableRow', 'tableColumn', 'mergeCell', 'splitCell' ]
+ *		};
+ *
+ * Of course, the same buttons can also be used in the
+ * {@link module:core/editor/editorconfig~EditorConfig#toolbar main editor toolbar}.
+ *
+ * Read more about configuring toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
+ *
+ * @member {Array.<String>} module:table~TableConfig#toolbar
+ */

+ 143 - 41
packages/ckeditor5-table/src/tableui.js

@@ -8,14 +8,26 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import { addListToDropdown, createDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+import Model from '@ckeditor/ckeditor5-ui/src/model';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
-import icon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import insertRowIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
-import insertColumnIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
+import InsertTableView from './ui/inserttableview';
+
+import tableIcon from './../theme/icons/table.svg';
+import tableColumnIcon from './../theme/icons/table-column.svg';
+import tableRowIcon from './../theme/icons/table-row.svg';
+import tableMergeCellIcon from './../theme/icons/table-merge-cell.svg';
 
 /**
- * The table UI plugin.
+ * The table UI plugin. It introduces:
+ *
+ * * The `'insertTable'` dropdown,
+ * * The `'tableColumn'` dropdown,
+ * * The `'tableRow'` dropdown,
+ * * The `'mergeCell'` dropdown.
+ *
+ * The `'tableColumn'`, `'tableRow'`, `'mergeCell'` work best with {@link module:table/tabletoolbar~TableToolbar}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -25,65 +37,155 @@ export default class TableUI extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
+		const t = this.editor.t;
 
 		editor.ui.componentFactory.add( 'insertTable', locale => {
 			const command = editor.commands.get( 'insertTable' );
-			const buttonView = new ButtonView( locale );
+			const dropdownView = createDropdown( locale );
 
-			buttonView.bind( 'isEnabled' ).to( command );
+			dropdownView.bind( 'isEnabled' ).to( command );
 
-			buttonView.set( {
-				icon,
-				label: 'Insert table',
+			// Decorate dropdown's button.
+			dropdownView.buttonView.set( {
+				icon: tableIcon,
+				label: t( 'Insert table' ),
 				tooltip: true
 			} );
 
-			buttonView.on( 'execute', () => {
-				editor.execute( 'insertTable' );
+			// Prepare custom view for dropdown's panel.
+			const insertTableView = new InsertTableView( locale );
+			dropdownView.panelView.children.add( insertTableView );
+
+			insertTableView.delegate( 'execute' ).to( dropdownView );
+
+			dropdownView.buttonView.on( 'open', () => {
+				// Reset the chooser before showing it to the user.
+				insertTableView.rows = 0;
+				insertTableView.columns = 0;
+			} );
+
+			dropdownView.on( 'execute', () => {
+				editor.execute( 'insertTable', { rows: insertTableView.rows, columns: insertTableView.columns } );
 				editor.editing.view.focus();
 			} );
 
-			return buttonView;
+			return dropdownView;
 		} );
 
-		editor.ui.componentFactory.add( 'insertRowBelow', locale => {
-			const command = editor.commands.get( 'insertRowBelow' );
-			const buttonView = new ButtonView( locale );
+		editor.ui.componentFactory.add( 'tableColumn', locale => {
+			const options = [
+				{ commandName: 'setColumnHeader', label: t( 'Header column' ), bindIsActive: true },
+				{ commandName: 'insertColumnBefore', label: t( 'Insert column before' ) },
+				{ commandName: 'insertColumnAfter', label: t( 'Insert column after' ) },
+				{ commandName: 'removeColumn', label: t( 'Delete column' ) }
+			];
 
-			buttonView.bind( 'isEnabled' ).to( command );
+			return this._prepareDropdown( 'Column', tableColumnIcon, options, locale );
+		} );
 
-			buttonView.set( {
-				icon: insertRowIcon,
-				label: 'Insert row',
-				tooltip: true
-			} );
+		editor.ui.componentFactory.add( 'tableRow', locale => {
+			const options = [
+				{ commandName: 'setRowHeader', label: t( 'Header row' ), bindIsActive: true },
+				{ commandName: 'insertRowBelow', label: t( 'Insert row below' ) },
+				{ commandName: 'insertRowAbove', label: t( 'Insert row above' ) },
+				{ commandName: 'removeRow', label: t( 'Delete row' ) }
+			];
 
-			buttonView.on( 'execute', () => {
-				editor.execute( 'insertRowBelow' );
-				editor.editing.view.focus();
-			} );
+			return this._prepareDropdown( 'Row', tableRowIcon, options, locale );
+		} );
 
-			return buttonView;
+		editor.ui.componentFactory.add( 'mergeCell', locale => {
+			const options = [
+				{ commandName: 'mergeCellUp', label: t( 'Merge cell up' ) },
+				{ commandName: 'mergeCellRight', label: t( 'Merge cell right' ) },
+				{ commandName: 'mergeCellDown', label: t( 'Merge cell down' ) },
+				{ commandName: 'mergeCellLeft', label: t( 'Merge cell left' ) },
+				{ commandName: 'splitCellVertically', label: t( 'Split cell vertically' ) },
+				{ commandName: 'splitCellHorizontally', label: t( 'Split cell horizontally' ) }
+			];
+
+			return this._prepareDropdown( 'Merge cell', tableMergeCellIcon, options, locale );
 		} );
+	}
 
-		editor.ui.componentFactory.add( 'insertColumnAfter', locale => {
-			const command = editor.commands.get( 'insertColumnAfter' );
-			const buttonView = new ButtonView( locale );
+	/**
+	 * Creates dropdown view from set of options.
+	 *
+	 * @private
+	 * @param {String} buttonName Dropdown button name.
+	 * @param {String} icon Icon for dropdown button.
+	 * @param {Array.<module:table/tableui~DropdownOption>} options List of options for dropdown.
+	 * @param {module:utils/locale~Locale} locale
+	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
+	 */
+	_prepareDropdown( buttonName, icon, options, locale ) {
+		const editor = this.editor;
 
-			buttonView.bind( 'isEnabled' ).to( command );
+		const dropdownView = createDropdown( locale );
+		const commands = [];
 
-			buttonView.set( {
-				icon: insertColumnIcon,
-				label: 'Insert column',
-				tooltip: true
-			} );
+		// Prepare dropdown list items for list dropdown.
+		const dropdownItems = new Collection();
 
-			buttonView.on( 'execute', () => {
-				editor.execute( 'insertColumnAfter' );
-				editor.editing.view.focus();
-			} );
+		for ( const option of options ) {
+			addListOption( option, editor, commands, dropdownItems );
+		}
+
+		addListToDropdown( dropdownView, dropdownItems );
+
+		// Decorate dropdown's button.
+		dropdownView.buttonView.set( {
+			label: buttonName,
+			icon,
+			tooltip: true
+		} );
+
+		// Make dropdown button disabled when all options are disabled.
+		dropdownView.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => {
+			return areEnabled.some( isEnabled => isEnabled );
+		} );
 
-			return buttonView;
+		this.listenTo( dropdownView, 'execute', evt => {
+			editor.execute( evt.source.commandName );
+			editor.editing.view.focus();
 		} );
+
+		return dropdownView;
 	}
 }
+
+// Adds an option to a list view.
+//
+// @param {module:table/tableui~DropdownOption} option Configuration option.
+// @param {module:core/editor/editor~Editor} editor
+// @param {Array.<module:core/command~Command>} commands List of commands to update.
+// @param {module:utils/collection~Collection} dropdownItems Collection of dropdown items to update with given option.
+function addListOption( option, editor, commands, dropdownItems ) {
+	const { commandName, label, bindIsActive } = option;
+	const command = editor.commands.get( commandName );
+
+	commands.push( command );
+
+	const itemModel = new Model( {
+		commandName,
+		label
+	} );
+
+	itemModel.bind( 'isEnabled' ).to( command );
+
+	if ( bindIsActive ) {
+		itemModel.bind( 'isActive' ).to( command, 'value' );
+	}
+
+	dropdownItems.add( itemModel );
+}
+
+/**
+ * Object describing table dropdowns' items.
+ *
+ * @typedef {Object} module:table/tableui~DropdownOption
+ * @private
+ * @property {String} commandName A command name to execute for that option.
+ * @property {String} label A dropdown item label.
+ * @property {Boolean} bindIsActive If `true` will bind command's value to `isActive` dropdown item property.
+ */

+ 183 - 0
packages/ckeditor5-table/src/ui/inserttableview.js

@@ -0,0 +1,183 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/ui/inserttableview
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+
+import './../../theme/inserttable.css';
+
+/**
+ * The table size view.
+ *
+ * It renders a 10x10 grid to choose inserted table size.
+ *
+ * @extends module:ui/view~View
+ */
+export default class InsertTableView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * Collection of the table size box items.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.items = this.createCollection();
+
+		/**
+		 * Currently selected number of rows of a new table.
+		 *
+		 * @observable
+		 * @member {Number} #rows
+		 */
+		this.set( 'rows', 0 );
+
+		/**
+		 * Currently selected number of columns of a new table.
+		 *
+		 * @observable
+		 * @member {Number} #columns
+		 */
+		this.set( 'columns', 0 );
+
+		/**
+		 * The label text displayed under the boxes.
+		 *
+		 * @observable
+		 * @member {String} #label
+		 */
+		this.bind( 'label' )
+			.to( this, 'columns', this, 'rows', ( columns, rows ) => `${ rows } x ${ columns }` );
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [ 'ck' ]
+			},
+
+			children: [
+				{
+					tag: 'div',
+					attributes: {
+						class: [ 'ck-insert-table-dropdown__grid' ]
+					},
+					children: this.items
+				},
+				{
+					tag: 'div',
+					attributes: {
+						class: [ 'ck-insert-table-dropdown__label' ]
+					},
+					children: [
+						{
+							text: bind.to( 'label' )
+						}
+					]
+				}
+			],
+
+			on: {
+				click: bind.to( () => {
+					this.fire( 'execute' );
+				} )
+			}
+		} );
+
+		// Add grid boxes to table selection view.
+		for ( let index = 0; index < 100; index++ ) {
+			const boxView = new TableSizeGridBoxView();
+
+			// Listen to box view 'over' event which indicates that mouse is over this box.
+			boxView.on( 'over', () => {
+				// Translate box index to the row & column index.
+				const row = Math.floor( index / 10 );
+				const column = index % 10;
+
+				// As row & column indexes are zero-based transform it to number of selected rows & columns.
+				this.set( 'rows', row + 1 );
+				this.set( 'columns', column + 1 );
+			} );
+
+			this.items.add( boxView );
+		}
+
+		this.on( 'change:columns', () => {
+			this._highlightGridBoxes();
+		} );
+
+		this.on( 'change:rows', () => {
+			this._highlightGridBoxes();
+		} );
+	}
+
+	/**
+	 * Highlights grid boxes depending on rows & columns selected.
+	 *
+	 * @private
+	 */
+	_highlightGridBoxes() {
+		const rows = this.rows;
+		const columns = this.columns;
+
+		this.items.map( ( boxView, index ) => {
+			// Translate box index to the row & column index.
+			const itemRow = Math.floor( index / 10 );
+			const itemColumn = index % 10;
+
+			// Grid box is highlighted when its row & column index belongs to selected number of rows & columns.
+			const isOn = itemRow < rows && itemColumn < columns;
+
+			boxView.set( 'isOn', isOn );
+		} );
+	}
+}
+
+/**
+ * A single grid box view element.
+ *
+ * This class is used to render table size selection grid in {@link module:table/ui/inserttableview~InsertTableView}
+ *
+ * @private
+ */
+class TableSizeGridBoxView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		/**
+		 * Controls whether the grid box view is "on".
+		 *
+		 * @observable
+		 * @member {Boolean} #isOn
+		 */
+		this.set( 'isOn', false );
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-insert-table-dropdown-grid-box',
+					bind.if( 'isOn', 'ck-on' )
+				]
+			},
+			on: {
+				mouseover: bind.to( 'over' )
+			}
+		} );
+	}
+}

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

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/ui/utils
+ */
+
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+import { getParentTable } from '../commands/utils';
+
+/**
+ * A helper utility that positions the
+ * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
+ * with respect to the table in the editor content, if one is selected.
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ */
+export function repositionContextualBalloon( editor ) {
+	const balloon = editor.plugins.get( 'ContextualBalloon' );
+
+	balloon.updatePosition( getBalloonPositionData( editor ) );
+}
+
+/**
+ * Returns the positioning options that controls the geometry of the
+ * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
+ * to the selected element in the editor content.
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ * @returns {module:utils/dom/position~Options}
+ */
+export function getBalloonPositionData( editor ) {
+	const editingView = editor.editing.view;
+	const defaultPositions = BalloonPanelView.defaultPositions;
+
+	const parentTable = getParentTable( editingView.document.selection.getFirstPosition() );
+
+	return {
+		target: editingView.domConverter.viewToDom( parentTable ),
+		positions: [
+			defaultPositions.northArrowSouth,
+			defaultPositions.northArrowSouthWest,
+			defaultPositions.northArrowSouthEast,
+			defaultPositions.southArrowNorth,
+			defaultPositions.southArrowNorthWest,
+			defaultPositions.southArrowNorthEast
+		]
+	};
+}

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

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/utils
+ */
+
+import { toWidget, isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
+import { getParentTable } from './commands/utils';
+
+const tableSymbol = Symbol( 'isImage' );
+
+/**
+ * Converts a given {@link module:engine/view/element~Element} to a table widget:
+ * * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the table widget element.
+ * * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
+ *
+ * @param {module:engine/view/element~Element} viewElement
+ * @param {module:engine/view/writer~Writer} writer An instance of the view writer.
+ * @param {String} label The element's label. It will be concatenated with the table `alt` attribute if one is present.
+ * @returns {module:engine/view/element~Element}
+ */
+export function toTableWidget( viewElement, writer ) {
+	writer.setCustomProperty( tableSymbol, true, viewElement );
+
+	return toWidget( viewElement, writer );
+}
+
+/**
+ * Checks if a given view element is a table widget.
+ *
+ * @param {module:engine/view/element~Element} viewElement
+ * @returns {Boolean}
+ */
+export function isTableWidget( viewElement ) {
+	return !!viewElement.getCustomProperty( tableSymbol ) && isWidget( viewElement );
+}
+
+/**
+ * Checks if a table widget is the only selected element.
+ *
+ * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} selection
+ * @returns {Boolean}
+ */
+export function isTableWidgetSelected( selection ) {
+	const parentTable = getParentTable( selection.getFirstPosition() );
+
+	return !!( parentTable && isTableWidget( parentTable ) );
+}

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

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

+ 103 - 87
packages/ckeditor5-table/tests/commands/settableheaderscommand.js

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

+ 86 - 0
packages/ckeditor5-table/tests/integration.js

@@ -0,0 +1,86 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloontoolbar';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Table from '../src/table';
+import TableToolbar from '../src/tabletoolbar';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+
+describe( 'TableToolbar integration', () => {
+	describe( 'with the BalloonToolbar', () => {
+		let balloon, balloonToolbar, newEditor, editorElement;
+
+		beforeEach( () => {
+			editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Table, TableToolbar, BalloonToolbar, Paragraph ]
+				} )
+				.then( editor => {
+					newEditor = editor;
+					balloon = newEditor.plugins.get( 'ContextualBalloon' );
+					balloonToolbar = newEditor.plugins.get( 'BalloonToolbar' );
+					const button = new View();
+
+					button.element = global.document.createElement( 'div' );
+
+					// There must be at least one toolbar items which is not disabled to show it.
+					// https://github.com/ckeditor/ckeditor5-ui/issues/269
+					balloonToolbar.toolbarView.items.add( button );
+
+					newEditor.editing.view.isFocused = true;
+					newEditor.editing.view.getDomRoot().focus();
+				} );
+		} );
+
+		afterEach( () => {
+			editorElement.remove();
+			return newEditor.destroy();
+		} );
+
+		it( 'should prevent the BalloonToolbar from being displayed when an table is selected', () => {
+			// When table is selected along with text.
+			setModelData( newEditor.model, '<paragraph>fo[o</paragraph><table><tableRow><tableCell></tableCell></tableRow></table>]' );
+
+			balloonToolbar.show();
+
+			// BalloonToolbar should be visible.
+			expect( balloon.visibleView ).to.equal( balloonToolbar.toolbarView );
+
+			// When only table is selected.
+			setModelData( newEditor.model, '<paragraph>foo</paragraph><table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			balloonToolbar.show();
+
+			// BalloonToolbar should not be visible.
+			expect( balloon.visibleView ).to.be.null;
+		} );
+
+		it( 'should listen to BalloonToolbar#show event with the high priority', () => {
+			const highestPrioritySpy = sinon.spy();
+			const highPrioritySpy = sinon.spy();
+			const normalPrioritySpy = sinon.spy();
+
+			// Select an table
+			setModelData( newEditor.model, '<paragraph>foo</paragraph><table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			newEditor.listenTo( balloonToolbar, 'show', highestPrioritySpy, { priority: 'highest' } );
+			newEditor.listenTo( balloonToolbar, 'show', highPrioritySpy, { priority: 'high' } );
+			newEditor.listenTo( balloonToolbar, 'show', normalPrioritySpy, { priority: 'normal' } );
+
+			balloonToolbar.show();
+
+			sinon.assert.calledOnce( highestPrioritySpy );
+			sinon.assert.notCalled( highPrioritySpy );
+			sinon.assert.notCalled( normalPrioritySpy );
+		} );
+	} );
+} );

+ 7 - 8
packages/ckeditor5-table/tests/manual/table.html

@@ -3,12 +3,12 @@
 	table {
 		border-collapse: collapse;
 		border-spacing: 0;
-		border-color: #000000;
+		border-color: hsl(0,0%,87%);
 	}
 
 	table, th, td {
 		padding: 5px;
-		border: 1px inset #000000;
+		border: 1px inset hsl(0,0%,87%);
 	}
 
 	table th,
@@ -17,20 +17,19 @@
 	}
 
 	table th {
-		background: #dadada;
-		font-weight: normal;
+		background: hsl(0,0%,96%);
+		font-weight: bold;
 	}
 
 	table thead th {
-		color: #ffffff;
-		background: #666666;
+		background: hsl(0,0%,90%);
 	}
 </style>
 
 <div id="editor">
 	<p>Table with everything:</p>
 	<table>
-		<caption>Data about the planets of our solar system (Planetary facts taken from <a href="http://nssdc.gsfc.nasa.gov/planetary/factsheet/" class="external external-icon" rel="noopener">Nasa's Planetary Fact Sheet - Metric</a>.</caption>
+		<caption>Data about the planets of our solar system (Planetary facts taken from <a href="http://nssdc.gsfc.nasa.gov/planetary/factsheet/">Nasa's Planetary Fact Sheet - Metric</a>.</caption>
 		<thead>
 		<tr>
 			<td colspan="2">&nbsp;</td>
@@ -158,7 +157,7 @@
 			<td>5906.4</td>
 			<td>-225</td>
 			<td>5</td>
-			<td>Declassified as a planet in 2006, but this <a href="http://www.usatoday.com/story/tech/2014/10/02/pluto-planet-solar-system/16578959/" class="external external-icon" rel="noopener">remains controversial</a>.</td>
+			<td>Declassified as a planet in 2006, but this <a href="http://www.usatoday.com/story/tech/2014/10/02/pluto-planet-solar-system/16578959/">remains controversial</a>.</td>
 		</tr>
 		</tbody>
 	</table>

+ 7 - 4
packages/ckeditor5-table/tests/manual/table.js

@@ -8,14 +8,17 @@
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import Table from '../../src/table';
+import TableToolbar from '../../src/tabletoolbar';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, Table ],
+		plugins: [ ArticlePluginSet, Table, TableToolbar ],
 		toolbar: [
-			'heading', '|', 'insertTable', 'insertRowBelow', 'insertColumnAfter',
-			'|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
-		]
+			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
+		],
+		table: {
+			toolbar: [ 'tableColumn', 'tableRow', 'mergeCell' ]
+		}
 	} )
 	.then( editor => {
 		window.editor = editor;

+ 64 - 0
packages/ckeditor5-table/tests/manual/table.md

@@ -1,3 +1,67 @@
 ### Loading
 
+1. The data should be loaded with:
+  * a complex table with:
+    - one heading row, 
+    - two heading columns, 
+    - merged cells in heading columns section,
+  * a table with 2 tbody sections in the DOM - should be rendered as a table with one tbody.
+  * a table with no tbody sections in the DOM - should be rendered as a table with one tbody.
+  * a table with a thead section between two tbody sections in dom - should be rendered as a table with one thead and on tbody section in proper order: 1, 2, 3.
+
+2. Main toolbar should have insert table dropdown.
+  
+3. While the table widget is selected there should be a toolbar attached to the table with 3 dropdowns:
+  * column dropdown with items:
+    - header column,
+    - insert column before,
+    - insert column after,
+    - delete column.
+  * row dropdown with items:
+    - header row,
+    - insert row below,
+    - insert row above,
+    - delete row.
+  * merge cell dropdown with items:
+    - merge cell up,
+    - merge cell right,
+    - merge cell down,
+    - merge cell left,
+    - split cell vertically,
+    - split cell horizontally,
+
 ### Testing
+
+Inserting table:
+
+1. Insert table of chosen size - the inserted table should have number columns & rows as selected in dropdown.
+2. Re-opening dropdown should reset selected table size.
+3. Table cannot be inserted into other table.
+
+Column manipulation:
+
+1. Insert column in table heading section.
+2. Insert column in merged cell.
+3. Insert column at the end/beginning of a table.
+4. Remove column from table heading section.
+5. Remove column of merged cell.
+6. Change column heading status.
+
+Column manipulation:
+
+1. Insert row in table heading section.
+2. Insert row in merged cell.
+3. Insert row at the end/beginning of a table.
+4. Remove row from table heading section.
+5. Remove row of merged cell.
+6. Change row heading status.
+
+Merging cells:
+
+1. Merge cell on the left/right/top/bottom of current cell.
+2. Merge cell on the left/right/top/bottom touching another table section (mergin a table cell from header row with a cell from body should not be possible).
+3. Merge cells that are already merged.
+
+Splitting cells:
+1. Split not merged cell vertically/horizontally.
+2. Split already merged cell vertically/horizontally.

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

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

+ 192 - 0
packages/ckeditor5-table/tests/tabletoolbar.js

@@ -0,0 +1,192 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import TableToolbar from '../src/tabletoolbar';
+import Table from '../src/table';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Range from '@ckeditor/ckeditor5-engine/src/model/range';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'TableToolbar', () => {
+	let editor, model, doc, editingView, plugin, toolbar, balloon, editorElement;
+
+	beforeEach( () => {
+		editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicEditor
+			.create( editorElement, {
+				plugins: [ Paragraph, Table, TableToolbar, FakeButton ],
+				table: {
+					toolbar: [ 'fake_button' ]
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = newEditor.model;
+				doc = model.document;
+				plugin = editor.plugins.get( TableToolbar );
+				toolbar = plugin._toolbar;
+				editingView = editor.editing.view;
+				balloon = editor.plugins.get( 'ContextualBalloon' );
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( TableToolbar ) ).to.be.instanceOf( TableToolbar );
+	} );
+
+	it( 'should not initialize if there is no configuration', () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicEditor.create( editorElement, {
+			plugins: [ TableToolbar ]
+		} )
+			.then( editor => {
+				expect( editor.plugins.get( TableToolbar )._toolbar ).to.be.undefined;
+
+				editorElement.remove();
+				return editor.destroy();
+			} );
+	} );
+
+	describe( 'toolbar', () => {
+		it( 'should use the config.table.toolbar to create items', () => {
+			expect( toolbar.items ).to.have.length( 1 );
+			expect( toolbar.items.get( 0 ).label ).to.equal( 'fake button' );
+		} );
+
+		it( 'should set proper CSS classes', () => {
+			const spy = sinon.spy( balloon, 'add' );
+
+			editor.ui.focusTracker.isFocused = true;
+
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			sinon.assert.calledWithMatch( spy, {
+				view: toolbar,
+				balloonClassName: 'ck-toolbar-container'
+			} );
+		} );
+	} );
+
+	describe( 'integration with the editor focus', () => {
+		it( 'should show the toolbar when the editor gains focus and the table is selected', () => {
+			editor.ui.focusTracker.isFocused = true;
+
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			editor.ui.focusTracker.isFocused = false;
+			expect( balloon.visibleView ).to.be.null;
+
+			editor.ui.focusTracker.isFocused = true;
+			expect( balloon.visibleView ).to.equal( toolbar );
+		} );
+
+		it( 'should hide the toolbar when the editor loses focus and the table is selected', () => {
+			editor.ui.focusTracker.isFocused = false;
+
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			editor.ui.focusTracker.isFocused = true;
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			editor.ui.focusTracker.isFocused = false;
+			expect( balloon.visibleView ).to.be.null;
+		} );
+	} );
+
+	describe( 'integration with the editor selection (#change event)', () => {
+		beforeEach( () => {
+			editor.ui.focusTracker.isFocused = true;
+		} );
+
+		it( 'should show the toolbar on render when the table is selected', () => {
+			setData( model, '<paragraph>[foo]</paragraph><table><tableRow><tableCell></tableCell></tableRow></table>' );
+
+			expect( balloon.visibleView ).to.be.null;
+
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.be.null;
+
+			model.change( writer => {
+				// Select the [<table></table>]
+				writer.setSelection(
+					Range.createOn( doc.getRoot().getChild( 1 ).getChild( 0 ).getChild( 0 ) )
+				);
+			} );
+
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			// Make sure successive change does not throw, e.g. attempting
+			// to insert the toolbar twice.
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.equal( toolbar );
+		} );
+
+		it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			const lastView = new View();
+			lastView.element = document.createElement( 'div' );
+
+			balloon.add( { view: lastView } );
+			expect( balloon.visibleView ).to.equal( lastView );
+
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.equal( lastView );
+		} );
+
+		it( 'should hide the toolbar on render if the table is de–selected', () => {
+			setData( model, '<paragraph>foo</paragraph><table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			model.change( writer => {
+				// Select the <paragraph>[...]</paragraph>
+				writer.setSelection(
+					Range.createIn( doc.getRoot().getChild( 0 ) )
+				);
+			} );
+
+			expect( balloon.visibleView ).to.be.null;
+
+			// Make sure successive change does not throw, e.g. attempting
+			// to remove the toolbar twice.
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.be.null;
+		} );
+	} );
+
+	// Plugin that adds fake_button to editor's component factory.
+	class FakeButton extends Plugin {
+		init() {
+			this.editor.ui.componentFactory.add( 'fake_button', locale => {
+				const view = new ButtonView( locale );
+
+				view.set( {
+					label: 'fake button'
+				} );
+
+				return view;
+			} );
+		}
+	}
+} );

+ 289 - 51
packages/ckeditor5-table/tests/tableui.js

@@ -7,11 +7,11 @@
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import { _clear as clearTranslations, add as addTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 import TableEditing from '../src/tableediting';
 import TableUI from '../src/tableui';
+import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
 
 testUtils.createSinonSandbox();
 
@@ -46,108 +46,346 @@ describe( 'TableUI', () => {
 		return editor.destroy();
 	} );
 
-	describe( 'insertTable button', () => {
+	describe( 'insertTable dropdown', () => {
 		let insertTable;
 
 		beforeEach( () => {
 			insertTable = editor.ui.componentFactory.create( 'insertTable' );
 		} );
 
-		it( 'should register insertTable buton', () => {
-			expect( insertTable ).to.be.instanceOf( ButtonView );
-			expect( insertTable.isOn ).to.be.false;
-			expect( insertTable.label ).to.equal( 'Insert table' );
-			expect( insertTable.icon ).to.match( /<svg / );
+		it( 'should register insertTable button', () => {
+			expect( insertTable ).to.be.instanceOf( DropdownView );
+			expect( insertTable.buttonView.label ).to.equal( 'Insert table' );
+			expect( insertTable.buttonView.icon ).to.match( /<svg / );
 		} );
 
 		it( 'should bind to insertTable command', () => {
 			const command = editor.commands.get( 'insertTable' );
 
 			command.isEnabled = true;
-			expect( insertTable.isOn ).to.be.false;
-			expect( insertTable.isEnabled ).to.be.true;
+			expect( insertTable.buttonView.isOn ).to.be.false;
+			expect( insertTable.buttonView.isEnabled ).to.be.true;
 
 			command.isEnabled = false;
-			expect( insertTable.isEnabled ).to.be.false;
+			expect( insertTable.buttonView.isEnabled ).to.be.false;
 		} );
 
 		it( 'should execute insertTable command on button execute event', () => {
 			const executeSpy = testUtils.sinon.spy( editor, 'execute' );
 
+			const tableSizeView = insertTable.panelView.children.get( 0 );
+
+			tableSizeView.rows = 2;
+			tableSizeView.columns = 7;
+
 			insertTable.fire( 'execute' );
 
 			sinon.assert.calledOnce( executeSpy );
-			sinon.assert.calledWithExactly( executeSpy, 'insertTable' );
+			sinon.assert.calledWithExactly( executeSpy, 'insertTable', { rows: 2, columns: 7 } );
+		} );
+
+		it( 'should reset rows & columns on dropdown open', () => {
+			const tableSizeView = insertTable.panelView.children.get( 0 );
+
+			expect( tableSizeView.rows ).to.equal( 0 );
+			expect( tableSizeView.columns ).to.equal( 0 );
+
+			tableSizeView.rows = 2;
+			tableSizeView.columns = 2;
+
+			insertTable.buttonView.fire( 'open' );
+
+			expect( tableSizeView.rows ).to.equal( 0 );
+			expect( tableSizeView.columns ).to.equal( 0 );
 		} );
 	} );
 
-	describe( 'insertRowBelow button', () => {
-		let insertRow;
+	describe( 'tableRow dropdown', () => {
+		let dropdown;
 
 		beforeEach( () => {
-			insertRow = editor.ui.componentFactory.create( 'insertRowBelow' );
+			dropdown = editor.ui.componentFactory.create( 'tableRow' );
 		} );
 
-		it( 'should register insertRowBelow button', () => {
-			expect( insertRow ).to.be.instanceOf( ButtonView );
-			expect( insertRow.isOn ).to.be.false;
-			expect( insertRow.label ).to.equal( 'Insert row' );
-			expect( insertRow.icon ).to.match( /<svg / );
+		it( 'have button with proper properties set', () => {
+			expect( dropdown ).to.be.instanceOf( DropdownView );
+
+			const button = dropdown.buttonView;
+
+			expect( button.isOn ).to.be.false;
+			expect( button.tooltip ).to.be.true;
+			expect( button.label ).to.equal( 'Row' );
+			expect( button.icon ).to.match( /<svg / );
 		} );
 
-		it( 'should bind to insertRow command', () => {
-			const command = editor.commands.get( 'insertRowBelow' );
+		it( 'should have proper items in panel', () => {
+			const listView = dropdown.listView;
 
-			command.isEnabled = true;
-			expect( insertRow.isOn ).to.be.false;
-			expect( insertRow.isEnabled ).to.be.true;
+			const labels = listView.items.map( ( { label } ) => label );
 
-			command.isEnabled = false;
-			expect( insertRow.isEnabled ).to.be.false;
+			expect( labels ).to.deep.equal( [ 'Header row', 'Insert row below', 'Insert row above', 'Delete row' ] );
 		} );
 
-		it( 'should execute insertRow command on button execute event', () => {
-			const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+		it( 'should bind items in panel to proper commands', () => {
+			const items = dropdown.listView.items;
 
-			insertRow.fire( 'execute' );
+			const setRowHeaderCommand = editor.commands.get( 'setRowHeader' );
+			const insertRowBelowCommand = editor.commands.get( 'insertRowBelow' );
+			const insertRowAboveCommand = editor.commands.get( 'insertRowAbove' );
+			const removeRowCommand = editor.commands.get( 'removeRow' );
 
-			sinon.assert.calledOnce( executeSpy );
-			sinon.assert.calledWithExactly( executeSpy, 'insertRowBelow' );
+			setRowHeaderCommand.isEnabled = true;
+			insertRowBelowCommand.isEnabled = true;
+			insertRowAboveCommand.isEnabled = true;
+			removeRowCommand.isEnabled = true;
+
+			expect( items.get( 0 ).isEnabled ).to.be.true;
+			expect( items.get( 1 ).isEnabled ).to.be.true;
+			expect( items.get( 2 ).isEnabled ).to.be.true;
+			expect( items.get( 3 ).isEnabled ).to.be.true;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			setRowHeaderCommand.isEnabled = false;
+
+			expect( items.get( 0 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			insertRowBelowCommand.isEnabled = false;
+
+			expect( items.get( 1 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			insertRowAboveCommand.isEnabled = false;
+			expect( items.get( 2 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			removeRowCommand.isEnabled = false;
+
+			expect( items.get( 3 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.false;
+		} );
+
+		it( 'should focus view after command execution', () => {
+			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
+
+			dropdown.listView.items.get( 0 ).fire( 'execute' );
+
+			sinon.assert.calledOnce( focusSpy );
+		} );
+
+		it( 'executes command when it\'s executed', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			dropdown.listView.items.get( 0 ).fire( 'execute' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'setRowHeader' );
+		} );
+
+		it( 'should bind set header row command value to dropdown item', () => {
+			const items = dropdown.listView.items;
+
+			const setRowHeaderCommand = editor.commands.get( 'setRowHeader' );
+
+			setRowHeaderCommand.value = false;
+			expect( items.get( 0 ).isActive ).to.be.false;
+
+			setRowHeaderCommand.value = true;
+			expect( items.get( 0 ).isActive ).to.be.true;
 		} );
 	} );
 
-	describe( 'insertColumnAfter button', () => {
-		let insertColumn;
+	describe( 'tableColumn dropdown', () => {
+		let dropdown;
 
 		beforeEach( () => {
-			insertColumn = editor.ui.componentFactory.create( 'insertColumnAfter' );
+			dropdown = editor.ui.componentFactory.create( 'tableColumn' );
 		} );
 
-		it( 'should register insertColumn buton', () => {
-			expect( insertColumn ).to.be.instanceOf( ButtonView );
-			expect( insertColumn.isOn ).to.be.false;
-			expect( insertColumn.label ).to.equal( 'Insert column' );
-			expect( insertColumn.icon ).to.match( /<svg / );
+		it( 'have button with proper properties set', () => {
+			expect( dropdown ).to.be.instanceOf( DropdownView );
+
+			const button = dropdown.buttonView;
+
+			expect( button.isOn ).to.be.false;
+			expect( button.tooltip ).to.be.true;
+			expect( button.label ).to.equal( 'Column' );
+			expect( button.icon ).to.match( /<svg / );
 		} );
 
-		it( 'should bind to insertColumn command', () => {
-			const command = editor.commands.get( 'insertColumnAfter' );
+		it( 'should have proper items in panel', () => {
+			const listView = dropdown.listView;
 
-			command.isEnabled = true;
-			expect( insertColumn.isOn ).to.be.false;
-			expect( insertColumn.isEnabled ).to.be.true;
+			const labels = listView.items.map( ( { label } ) => label );
 
-			command.isEnabled = false;
-			expect( insertColumn.isEnabled ).to.be.false;
+			expect( labels ).to.deep.equal( [ 'Header column', 'Insert column before', 'Insert column after', 'Delete column' ] );
 		} );
 
-		it( 'should execute insertColumn command on button execute event', () => {
-			const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+		it( 'should bind items in panel to proper commands', () => {
+			const items = dropdown.listView.items;
 
-			insertColumn.fire( 'execute' );
+			const setColumnHeaderCommand = editor.commands.get( 'setColumnHeader' );
+			const insertColumnBeforeCommand = editor.commands.get( 'insertColumnBefore' );
+			const insertColumnAfterCommand = editor.commands.get( 'insertColumnAfter' );
+			const removeColumnCommand = editor.commands.get( 'removeColumn' );
 
-			sinon.assert.calledOnce( executeSpy );
-			sinon.assert.calledWithExactly( executeSpy, 'insertColumnAfter' );
+			setColumnHeaderCommand.isEnabled = true;
+			insertColumnBeforeCommand.isEnabled = true;
+			insertColumnAfterCommand.isEnabled = true;
+			removeColumnCommand.isEnabled = true;
+
+			expect( items.get( 0 ).isEnabled ).to.be.true;
+			expect( items.get( 1 ).isEnabled ).to.be.true;
+			expect( items.get( 2 ).isEnabled ).to.be.true;
+			expect( items.get( 3 ).isEnabled ).to.be.true;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			setColumnHeaderCommand.isEnabled = false;
+
+			expect( items.get( 0 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			insertColumnBeforeCommand.isEnabled = false;
+
+			expect( items.get( 1 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			insertColumnAfterCommand.isEnabled = false;
+			expect( items.get( 2 ).isEnabled ).to.be.false;
+
+			removeColumnCommand.isEnabled = false;
+			expect( items.get( 3 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.false;
+		} );
+
+		it( 'should focus view after command execution', () => {
+			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
+
+			dropdown.listView.items.get( 0 ).fire( 'execute' );
+
+			sinon.assert.calledOnce( focusSpy );
+		} );
+
+		it( 'executes command when it\'s executed', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			dropdown.listView.items.get( 0 ).fire( 'execute' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'setColumnHeader' );
+		} );
+
+		it( 'should bind set header column command value to dropdown item', () => {
+			const items = dropdown.listView.items;
+
+			const setColumnHeaderCommand = editor.commands.get( 'setColumnHeader' );
+
+			setColumnHeaderCommand.value = false;
+			expect( items.get( 0 ).isActive ).to.be.false;
+
+			setColumnHeaderCommand.value = true;
+			expect( items.get( 0 ).isActive ).to.be.true;
+		} );
+	} );
+
+	describe( 'mergeCell dropdown', () => {
+		let dropdown;
+
+		beforeEach( () => {
+			dropdown = editor.ui.componentFactory.create( 'mergeCell' );
+		} );
+
+		it( 'have button with proper properties set', () => {
+			expect( dropdown ).to.be.instanceOf( DropdownView );
+
+			const button = dropdown.buttonView;
+
+			expect( button.isOn ).to.be.false;
+			expect( button.tooltip ).to.be.true;
+			expect( button.label ).to.equal( 'Merge cell' );
+			expect( button.icon ).to.match( /<svg / );
+		} );
+
+		it( 'should have proper items in panel', () => {
+			const listView = dropdown.listView;
+
+			const labels = listView.items.map( ( { label } ) => label );
+
+			expect( labels ).to.deep.equal( [
+				'Merge cell up',
+				'Merge cell right',
+				'Merge cell down',
+				'Merge cell left',
+				'Split cell vertically',
+				'Split cell horizontally'
+			] );
+		} );
+
+		it( 'should bind items in panel to proper commands', () => {
+			const items = dropdown.listView.items;
+
+			const mergeCellUpCommand = editor.commands.get( 'mergeCellUp' );
+			const mergeCellRightCommand = editor.commands.get( 'mergeCellRight' );
+			const mergeCellDownCommand = editor.commands.get( 'mergeCellDown' );
+			const mergeCellLeftCommand = editor.commands.get( 'mergeCellLeft' );
+			const splitCellVerticallyCommand = editor.commands.get( 'splitCellVertically' );
+			const splitCellHorizontallyCommand = editor.commands.get( 'splitCellHorizontally' );
+
+			mergeCellUpCommand.isEnabled = true;
+			mergeCellRightCommand.isEnabled = true;
+			mergeCellDownCommand.isEnabled = true;
+			mergeCellLeftCommand.isEnabled = true;
+			splitCellVerticallyCommand.isEnabled = true;
+			splitCellHorizontallyCommand.isEnabled = true;
+
+			expect( items.get( 0 ).isEnabled ).to.be.true;
+			expect( items.get( 1 ).isEnabled ).to.be.true;
+			expect( items.get( 2 ).isEnabled ).to.be.true;
+			expect( items.get( 3 ).isEnabled ).to.be.true;
+			expect( items.get( 4 ).isEnabled ).to.be.true;
+			expect( items.get( 5 ).isEnabled ).to.be.true;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			mergeCellUpCommand.isEnabled = false;
+
+			expect( items.get( 0 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			mergeCellRightCommand.isEnabled = false;
+
+			expect( items.get( 1 ).isEnabled ).to.be.false;
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+
+			mergeCellDownCommand.isEnabled = false;
+			expect( items.get( 2 ).isEnabled ).to.be.false;
+
+			mergeCellLeftCommand.isEnabled = false;
+			expect( items.get( 3 ).isEnabled ).to.be.false;
+
+			splitCellVerticallyCommand.isEnabled = false;
+			expect( items.get( 4 ).isEnabled ).to.be.false;
+
+			splitCellHorizontallyCommand.isEnabled = false;
+			expect( items.get( 5 ).isEnabled ).to.be.false;
+
+			expect( dropdown.buttonView.isEnabled ).to.be.false;
+		} );
+
+		it( 'should focus view after command execution', () => {
+			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
+
+			dropdown.listView.items.get( 0 ).fire( 'execute' );
+
+			sinon.assert.calledOnce( focusSpy );
+		} );
+
+		it( 'executes command when it\'s executed', () => {
+			const spy = sinon.stub( editor, 'execute' );
+
+			dropdown.listView.items.get( 0 ).fire( 'execute' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.equal( 'mergeCellUp' );
 		} );
 	} );
 } );

+ 126 - 0
packages/ckeditor5-table/tests/ui/inserttableview.js

@@ -0,0 +1,126 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document, Event */
+
+import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
+
+import InsertTableView from '../../src/ui/inserttableview';
+
+describe( 'InsertTableView', () => {
+	let view, locale;
+
+	beforeEach( () => {
+		locale = { t() {} };
+
+		view = new InsertTableView( locale );
+		view.render();
+
+		document.body.appendChild( view.element );
+	} );
+
+	afterEach( () => {
+		view.element.remove();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'sets view#locale', () => {
+			expect( view.locale ).to.equal( locale );
+		} );
+
+		it( 'sets view#rows to 0', () => {
+			expect( view.rows ).to.equal( 0 );
+		} );
+
+		it( 'sets view#columns to 0', () => {
+			expect( view.columns ).to.equal( 0 );
+		} );
+
+		it( 'sets #label to default rows & columns', () => {
+			expect( view.label ).to.equal( '0 x 0' );
+		} );
+
+		it( 'creates #element from template', () => {
+			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( view.element.children ).to.have.length( 2 );
+			expect( view.element.children[ 0 ].classList.contains( 'ck-insert-table-dropdown__grid' ) ).to.be.true;
+			expect( view.element.children[ 1 ].classList.contains( 'ck-insert-table-dropdown__label' ) ).to.be.true;
+		} );
+
+		it( 'creates view#items collection', () => {
+			expect( view.items ).to.be.instanceOf( ViewCollection );
+			expect( view.items ).to.have.length( 100 );
+		} );
+
+		describe( 'view#items bindings', () => {
+			it( 'updates view#height & view#width on "over" event', () => {
+				const boxView = view.items.get( 0 );
+
+				expect( boxView.isOn ).to.be.false;
+
+				boxView.fire( 'over' );
+
+				expect( boxView.isOn ).to.be.true;
+
+				expect( view.rows ).to.equal( 1 );
+				expect( view.columns ).to.equal( 1 );
+
+				const boxViewB = view.items.get( 22 );
+
+				boxViewB.fire( 'over' );
+
+				expect( view.rows ).to.equal( 3 );
+				expect( view.columns ).to.equal( 3 );
+			} );
+		} );
+
+		describe( 'bindings', () => {
+			it( 'binds #label to rows & columns', () => {
+				view.rows = 3;
+
+				expect( view.label ).to.equal( '3 x 0' );
+
+				view.columns = 7;
+
+				expect( view.label ).to.equal( '3 x 7' );
+			} );
+
+			describe( 'DOM', () => {
+				it( 'fires execute on "click" event', () => {
+					const spy = sinon.spy();
+
+					view.on( 'execute', spy );
+
+					dispatchEvent( view.element, 'click' );
+
+					sinon.assert.calledOnce( spy );
+				} );
+
+				describe( 'view#items mousemove event', () => {
+					it( 'fires "over" event', () => {
+						const boxView = view.items.get( 0 );
+						const spy = sinon.spy();
+
+						boxView.on( 'over', spy );
+
+						dispatchEvent( boxView.element, 'mouseover' );
+
+						sinon.assert.calledOnce( spy );
+					} );
+				} );
+			} );
+		} );
+	} );
+} );
+
+function dispatchEvent( el, domEvtName ) {
+	if ( !el.parentNode ) {
+		throw new Error( 'To dispatch an event, element must be in DOM. Otherwise #target is null.' );
+	}
+
+	el.dispatchEvent( new Event( domEvtName, {
+		bubbles: true
+	} ) );
+}

+ 1 - 0
packages/ckeditor5-table/theme/icons/table-column.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="#333" fill-rule="evenodd"><path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1zM2 2v16h16V2H2z" opacity=".6"/><path d="M18 7v1H2V7h16zm0 5v1H2v-1h16z" fill-rule="nonzero" opacity=".6"/><path d="M14 1v18a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1zm-2 1H8v4h4V2zm0 6H8v4h4V8zm0 6H8v4h4v-4z"/></g></svg>

+ 1 - 0
packages/ckeditor5-table/theme/icons/table-merge-cell.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="#333" fill-rule="evenodd"><path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1zM2 2v16h16V2H2z" opacity=".6"/><path d="M7 2h1v16H7V2zm5 0h1v7h-1V2zm6 5v1H2V7h16zM8 12v1H2v-1h6z" fill-rule="nonzero" opacity=".6"/><path d="M7 7h12a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1zm1 2v9h10V9H8z"/></g></svg>

+ 1 - 0
packages/ckeditor5-table/theme/icons/table-row.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="#333" fill-rule="evenodd"><path d="M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1zM2 2v16h16V2H2z" opacity=".6"/><path d="M7 2h1v16H7V2zm5 0h1v16h-1V2z" fill-rule="nonzero" opacity=".6"/><path d="M1 6h18a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm1 2v4h4V8H2zm6 0v4h4V8H8zm6 0v4h4V8h-4z"/></g></svg>

+ 1 - 0
packages/ckeditor5-table/theme/icons/table.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="#333" fill-rule="evenodd"><path d="M2.5 3v13.5h15V3H19v13.5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 16.5V3h1.5z"/><path d="M12 13H8v5H7v-5H1v-1h6V9H1V8h6V4.5H1v-1a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v1h-6V8h6v1h-6v3h6v1h-6v5h-1v-5zm0-1V9H8v3h4zm0-4V4.5H8V8h4z" fill-rule="nonzero"/></g></svg>

+ 41 - 0
packages/ckeditor5-table/theme/inserttable.css

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+:root {
+	--ck-insert-table-dropdown-padding: 10px;
+	--ck-insert-table-dropdown-box-height: 11px;
+	--ck-insert-table-dropdown-box-width: 12px;
+	--ck-insert-table-dropdown-box-margin: 1px;
+	--ck-insert-table-dropdown-box-border-color: hsl(0, 0%, 75%);
+	--ck-insert-table-dropdown-box-border-active-color: hsl(208, 73%, 61%);
+	--ck-insert-table-dropdown-box-active-background: hsl(208, 100%, 89%);
+}
+
+.ck .ck-insert-table-dropdown__grid {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: wrap;
+	/* The width of a container should match 10 items in a row so there will be a 10x10 grid. */
+	width: calc(var(--ck-insert-table-dropdown-box-width) * 10 + var(--ck-insert-table-dropdown-box-margin) * 20 + var(--ck-insert-table-dropdown-padding) * 2);
+	padding: var(--ck-insert-table-dropdown-padding) var(--ck-insert-table-dropdown-padding) 0;
+}
+
+.ck .ck-insert-table-dropdown__label {
+	text-align: center;
+}
+
+.ck .ck-insert-table-dropdown-grid-box {
+	width: var(--ck-insert-table-dropdown-box-width);
+	height: var(--ck-insert-table-dropdown-box-height);
+	margin: var(--ck-insert-table-dropdown-box-margin);
+	border: 1px solid var(--ck-insert-table-dropdown-box-border-color);
+	border-radius: 1px;
+
+	&.ck-on {
+		border-color: var(--ck-insert-table-dropdown-box-border-active-color);
+		background: var(--ck-insert-table-dropdown-box-active-background);
+	}
+}
+