Browse Source

Merge branch 'master' into t/114

Piotrek Koszuliński 7 years ago
parent
commit
d57eebdf76

+ 1 - 1
packages/ckeditor5-table/docs/_snippets/features/table.js

@@ -17,7 +17,7 @@ ClassicEditor
 			viewportTopOffset: 60
 		},
 		table: {
-			toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
 		}
 	} )
 	.then( editor => {

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

@@ -13,6 +13,10 @@ The {@link module:table/table~Table} feature offers table creation and editing t
 
 ## Installation
 
+<info-box info>
+	This feature is enabled by default in all builds. The installation instructions are for developers interested in building their own, custom editor.
+</info-box>
+
 To add this feature to your editor, install the [`@ckeditor/ckeditor5-table`](https://www.npmjs.com/package/@ckeditor/ckeditor5-table) package:
 
 ```bash
@@ -27,20 +31,16 @@ import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ Table, TableToolbar, ... ],
-		toolbar: [ 'insertTable', ... ]
+		plugins: [ Table, TableToolbar, Bold, ... ],
+		toolbar: [ 'insertTable', ... ],
 		table: {
-			toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
 		}
 	} )
 	.then( ... )
 	.catch( ... );
 ```
 
-<info-box info>
-	At the moment by default the table feature is available only in the {@link builds/guides/overview#document-editor document editor build}. Read more about {@link builds/guides/integration/installing-plugins installing plugins} if you want to add it to other editor builds.
-</info-box>
-
 ## Common API
 
 The {@link module:table/table~Table} plugin registers the following UI components:
@@ -68,7 +68,9 @@ And the following commands:
 * The `'splitTableCellVertically'` command implemented by {@link module:table/commands/splitcellcommand~SplitCellCommand}.
 * The `'splitTableCellHorizontally'` command implemented by {@link module:table/commands/splitcellcommand~SplitCellCommand}.
 
-The {@link module:table/tabletoolbar~TableToolbar} plugin introduces the balloon toolbar for tables. The toolbar shows up when a table cell is selected and is anchored to the table. It is possible to {@link module:table/table~TableConfig#toolbar configure} its content. Normally, it contains the table-related tools such as `'tableColumn'`, `'tableRow'`, and `'mergeTableCells'` dropdowns.
+The {@link module:table/tabletoolbar~TableToolbar} plugin introduces two balloon toolbars for tables.
+* The content toolbar shows up when table cell is selected and is anchored to the table. It is possible to {@link module:table/table~TableConfig#contentToolbar configure} its content. Normally, it contains the table-related tools such as `'tableColumn'`, `'tableRow'`, and `'mergeTableCells'` dropdowns.
+* The table toolbar shows up when the whole table is selected, for instance using the widget handler. It is possible to {@link module:table/table~TableConfig#tableToolbar configure} its content.
 
 ## Block vs inline content in table cells
 

+ 63 - 119
packages/ckeditor5-table/src/tabletoolbar.js

@@ -8,21 +8,20 @@
  */
 
 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, isTableContentSelected } from './utils';
-import { repositionContextualBalloon, getBalloonPositionData } from './ui/utils';
-
-const balloonClassName = 'ck-toolbar-container';
+import { isTableContentSelected, isTableWidgetSelected } from './utils';
+import WidgetToolbarRepository from '@ckeditor/ckeditor5-widget/src/widgettoolbarrepository';
 
 /**
- * The table toolbar class. It creates a table toolbar that shows up when the table widget is selected.
+ * The table toolbar class. It creates toolbars for the table feature and its content (for now only for a table cell content).
+ *
+ * Table toolbar shows up when a table widget is selected. Its components (e.g. buttons) are created based on the
+ * {@link module:table/table~TableConfig#toolbar `table.tableToolbar` configuration option}.
  *
- * Instanecs of toolbar components (e.g. buttons) are created using the editor's
- * {@link module:ui/componentfactory~ComponentFactory component factory}
- * based on the {@link module:table/table~TableConfig#toolbar `table.toolbar` configuration option}.
+ * Table content toolbar shows up when the selection is inside the content of a table. It creates its component based on the
+ * {@link module:table/table~TableConfig#contentToolbar `table.contentToolbar` configuration option}.
  *
- * The toolbar uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon} plugin.
+ * Note that the old {@link module:table/table~TableConfig#toolbar `table.toolbar` configuration option} is deprecated
+ * and will be removed in the next major release.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -31,7 +30,7 @@ export default class TableToolbar extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ContextualBalloon ];
+		return [ WidgetToolbarRepository ];
 	}
 
 	/**
@@ -44,126 +43,53 @@ export default class TableToolbar extends Plugin {
 	/**
 	 * @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 toolbar view 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 );
+		const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
 
-		// Show balloon panel each time table widget is selected.
-		this.listenTo( editor.ui, 'update', () => {
-			this._checkIsVisible();
-		} );
+		const tableContentToolbarItems = editor.config.get( 'table.contentToolbar' );
+		const deprecatedTableContentToolbarItems = editor.config.get( 'table.toolbar' );
 
-		// 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;
-		const viewSelection = editor.editing.view.document.selection;
+		const tableToolbarItems = editor.config.get( 'table.tableToolbar' );
 
-		if ( !editor.ui.focusTracker.isFocused || !isTableContentSelected( viewSelection ) ) {
-			this._hideToolbar();
-		} else {
-			this._showToolbar();
+		if ( deprecatedTableContentToolbarItems ) {
+			// eslint-disable-next-line
+			console.warn(
+				'`config.table.toolbar` is deprecated and will be removed in the next major release.' +
+				' Use `config.table.contentToolbar` instead.'
+			);
 		}
-	}
 
-	/**
-	 * 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
+		if ( tableContentToolbarItems || deprecatedTableContentToolbarItems ) {
+			widgetToolbarRepository.register( 'tableContent', {
+				items: tableContentToolbarItems || deprecatedTableContentToolbarItems,
+				visibleWhen: isTableContentSelected,
 			} );
 		}
-	}
 
-	/**
-	 * Removes the {@link #_toolbar} from the {@link #_balloon}.
-	 *
-	 * @private
-	 */
-	_hideToolbar() {
-		if ( !this._isVisible ) {
-			return;
+		if ( tableToolbarItems ) {
+			widgetToolbarRepository.register( 'table', {
+				items: tableToolbarItems,
+				visibleWhen: isTableWidgetSelected,
+			} );
 		}
-
-		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.
+ * Items to be placed in the table content toolbar.
+ *
+ * **Note:** This is a deprecated configuration option! Use {@link module:table/table~TableConfig#contentToolbar} instead.
+ *
+ * Read more about configuring toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
+ *
+ * @deprecated
+ * @member {Array.<String>} module:table/table~TableConfig#toolbar
+ */
+
+/**
+ * Items to be placed in the table content toolbar.
+ * The {@link module:table/tabletoolbar~TableToolbar} plugin is required to make this toolbar working.
  *
  * Assuming that you use the {@link module:table/tableui~TableUI} feature, the following toolbar items will be available
  * in {@link module:ui/componentfactory~ComponentFactory}:
@@ -175,7 +101,7 @@ export default class TableToolbar extends Plugin {
  * You can thus configure the toolbar like this:
  *
  *		const tableConfig = {
- *			toolbar: [ 'tableRow', 'tableColumn', 'mergeTableCells' ]
+ *			contentToolbar: [ 'tableRow', 'tableColumn', 'mergeTableCells' ]
  *		};
  *
  * Of course, the same buttons can also be used in the
@@ -183,5 +109,23 @@ export default class TableToolbar extends Plugin {
  *
  * Read more about configuring toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
  *
- * @member {Array.<String>} module:table/table~TableConfig#toolbar
+ * @member {Array.<String>} module:table/table~TableConfig#contentToolbar
+ */
+
+/**
+ * Items to be placed in the table toolbar.
+ * The {@link module:table/tabletoolbar~TableToolbar} plugin is required to make this toolbar working.
+ *
+ * You can thus configure the toolbar like this:
+ *
+ *		const tableConfig = {
+ *			tableToolbar: [ 'blockQuote' ]
+ *		};
+ *
+ * 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/table~TableConfig#tableToolbar
  */

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

@@ -1,52 +0,0 @@
-/**
- * @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 { findAncestor } 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 control 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 viewSelection = editingView.document.selection;
-
-	const parentTable = findAncestor( 'table', viewSelection.getFirstPosition() );
-
-	return {
-		target: editingView.domConverter.viewToDom( parentTable ),
-		positions: [
-			defaultPositions.northArrowSouth,
-			defaultPositions.northArrowSouthWest,
-			defaultPositions.northArrowSouthEast,
-			defaultPositions.southArrowNorth,
-			defaultPositions.southArrowNorthWest,
-			defaultPositions.southArrowNorthEast
-		]
-	};
-}

+ 1 - 1
packages/ckeditor5-table/tests/integration.js

@@ -12,7 +12,7 @@ import Table from '../src/table';
 import TableToolbar from '../src/tabletoolbar';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
-describe( 'TableToolbar integration', () => {
+describe( 'TableContentToolbar integration', () => {
 	describe( 'with the BalloonToolbar', () => {
 		let balloon, balloonToolbar, newEditor, editorElement;
 

+ 2 - 1
packages/ckeditor5-table/tests/manual/table.js

@@ -17,7 +17,8 @@ ClassicEditor
 			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
 		],
 		table: {
-			toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ],
+			tableToolbar: [ 'bold', 'italic' ]
 		}
 	} )
 	.then( editor => {

+ 6 - 4
packages/ckeditor5-table/tests/manual/table.md

@@ -2,16 +2,16 @@
 
 1. The data should be loaded with:
   * a complex table with:
-    - one heading row, 
-    - two heading columns, 
+    - 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:
+
+3. While the table cell is selected there should be a toolbar attached to the table with 3 dropdowns:
   * column dropdown with items:
     - header column,
     - insert column before,
@@ -30,6 +30,8 @@
     - split cell vertically,
     - split cell horizontally,
 
+4. While the table widget is selected there should be `bold` and `italic` buttons
+
 ### Testing
 
 Inserting table:

+ 1 - 1
packages/ckeditor5-table/tests/manual/tableblockcontent.js

@@ -19,7 +19,7 @@ ClassicEditor
 			'alignment', '|', 'undo', 'redo'
 		],
 		table: {
-			toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
+			contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
 		}
 	} )
 	.then( editor => {

+ 2 - 2
packages/ckeditor5-table/tests/manual/tableblockcontent.md

@@ -13,7 +13,7 @@
 
 ### Testing
 
-1. Use <kbd>Enter</kbd> in cells with single `<pargraph>`. When two `<paragraph>`'s are in one table cell they should be rendered as `<p>`.
+1. Use <kbd>Enter</kbd> in cells with single `<paragraph>`. When two `<paragraph>`'s are in one table cell they should be rendered as `<p>`.
 2. Undo previous step - the `<p>` element should be changed to `<span>` for single paragraph.
 3. Change `<heading>` to paragraph - it should be rendered as `<p>` element if there are other headings or other block content.
-4. Change one `<heading>` to paragraph and remove other headings. The `<paragraph>` should be rendered as `<span>`. 
+4. Change one `<heading>` to paragraph and remove other headings. The `<paragraph>` should be rendered as `<span>`.

+ 383 - 132
packages/ckeditor5-table/tests/tabletoolbar.js

@@ -3,9 +3,9 @@
  * For licensing, see LICENSE.md.
  */
 
-/* global document */
+/* global document, window */
 
-import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ClassicTestEditor 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';
@@ -15,199 +15,450 @@ 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';
+import WidgetToolbarRepository from '@ckeditor/ckeditor5-widget/src/widgettoolbarrepository';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 describe( 'TableToolbar', () => {
-	let editor, model, doc, plugin, toolbar, balloon, editorElement;
+	testUtils.createSinonSandbox();
 
-	beforeEach( () => {
-		editorElement = global.document.createElement( 'div' );
-		global.document.body.appendChild( editorElement );
+	describe( 'contentToolbar', () => {
+		let editor, model, doc, widgetToolbarRepository, toolbar, balloon, editorElement;
 
-		return ClassicEditor
-			.create( editorElement, {
-				plugins: [ Paragraph, Table, TableToolbar, FakeButton ],
-				table: {
-					toolbar: [ 'fake_button' ]
-				}
+		beforeEach( () => {
+			editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Paragraph, Table, TableToolbar, FakeButton ],
+					table: {
+						contentToolbar: [ 'fake_button' ]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = newEditor.model;
+					doc = model.document;
+					widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
+					toolbar = widgetToolbarRepository._toolbars.get( 'tableContent' ).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 ClassicTestEditor.create( editorElement, {
+				plugins: [ TableToolbar ]
 			} )
-			.then( newEditor => {
-				editor = newEditor;
-				model = newEditor.model;
-				doc = model.document;
-				plugin = editor.plugins.get( TableToolbar );
-				toolbar = plugin._toolbar;
-				balloon = editor.plugins.get( 'ContextualBalloon' );
+				.then( editor => {
+					const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
+					expect( widgetToolbarRepository._toolbars.get( 'tableContent' ) ).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' );
 			} );
-	} );
 
-	afterEach( () => {
-		editorElement.remove();
+			it( 'should set proper CSS classes', () => {
+				const spy = sinon.spy( balloon, 'add' );
 
-		return editor.destroy();
-	} );
+				editor.ui.focusTracker.isFocused = true;
 
-	it( 'should be loaded', () => {
-		expect( editor.plugins.get( TableToolbar ) ).to.be.instanceOf( TableToolbar );
-	} );
+				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;
 
-	it( 'should not initialize if there is no configuration', () => {
-		const editorElement = global.document.createElement( 'div' );
-		global.document.body.appendChild( editorElement );
+				setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
 
-		return ClassicEditor.create( editorElement, {
-			plugins: [ TableToolbar ]
-		} )
-			.then( editor => {
-				expect( editor.plugins.get( TableToolbar )._toolbar ).to.be.undefined;
+				editor.ui.focusTracker.isFocused = false;
+				expect( balloon.visibleView ).to.be.null;
 
-				editorElement.remove();
-				return editor.destroy();
+				editor.ui.focusTracker.isFocused = true;
+				expect( balloon.visibleView ).to.equal( toolbar );
 			} );
-	} );
 
-	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 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;
+			} );
 		} );
 
-		it( 'should set proper CSS classes', () => {
-			const spy = sinon.spy( balloon, 'add' );
+		describe( 'integration with the editor selection (ui#update event)', () => {
+			beforeEach( () => {
+				editor.ui.focusTracker.isFocused = true;
+			} );
+
+			it( 'should not show the toolbar on ui#update when the table is selected', () => {
+				setData( model, '<paragraph>foo</paragraph>[<table><tableRow><tableCell></tableCell></tableRow></table>]' );
+
+				expect( balloon.visibleView ).to.be.null;
+			} );
+
+			it( 'should show the toolbar on ui#update when the table content is selected', () => {
+				setData(
+					model,
+					'<paragraph>[foo]</paragraph><table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
+				);
+
+				expect( balloon.visibleView ).to.be.null;
 
-			editor.ui.focusTracker.isFocused = true;
+				editor.ui.fire( 'update' );
 
-			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+				expect( balloon.visibleView ).to.be.null;
+
+				model.change( writer => {
+					// Select the <tableCell>[bar]</tableCell>
+					writer.setSelection(
+						Range.createOn( doc.getRoot().getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 ) )
+					);
+				} );
 
-			sinon.assert.calledWithMatch( spy, {
-				view: toolbar,
-				balloonClassName: 'ck-toolbar-container'
+				expect( balloon.visibleView ).to.equal( toolbar );
+
+				// Make sure successive change does not throw, e.g. attempting
+				// to insert the toolbar twice.
+				editor.ui.fire( 'update' );
+				expect( balloon.visibleView ).to.equal( toolbar );
 			} );
-		} );
-	} );
 
-	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;
+			it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
+				setData( model, '<table><tableRow><tableCell><paragraph>x[y]z</paragraph></tableCell></tableRow></table>' );
 
-			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+				expect( balloon.visibleView ).to.equal( toolbar );
 
-			editor.ui.focusTracker.isFocused = false;
-			expect( balloon.visibleView ).to.be.null;
+				// Put anything on top of the ContextualBalloon stack above the table toolbar.
+				const lastView = new View();
+				lastView.element = document.createElement( 'div' );
 
-			editor.ui.focusTracker.isFocused = true;
-			expect( balloon.visibleView ).to.equal( toolbar );
-		} );
+				balloon.add( {
+					view: lastView,
+					position: {
+						target: document.body
+					}
+				} );
+
+				expect( balloon.visibleView ).to.equal( lastView );
+
+				editor.ui.fire( 'update' );
+
+				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><paragraph>[]</paragraph></tableCell></tableRow></table>'
+				);
 
-		it( 'should hide the toolbar when the editor loses focus and the table is selected', () => {
-			editor.ui.focusTracker.isFocused = false;
+				expect( balloon.visibleView ).to.equal( toolbar );
 
-			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+				model.change( writer => {
+					// Select the <paragraph>[...]</paragraph>
+					writer.setSelection(
+						Range.createIn( doc.getRoot().getChild( 0 ) )
+					);
+				} );
 
-			editor.ui.focusTracker.isFocused = true;
-			expect( balloon.visibleView ).to.equal( toolbar );
+				expect( balloon.visibleView ).to.be.null;
 
-			editor.ui.focusTracker.isFocused = false;
-			expect( balloon.visibleView ).to.be.null;
+				// Make sure successive change does not throw, e.g. attempting
+				// to remove the toolbar twice.
+				editor.ui.fire( 'update' );
+				expect( balloon.visibleView ).to.be.null;
+			} );
 		} );
 	} );
 
-	describe( 'integration with the editor selection (ui#update event)', () => {
-		beforeEach( () => {
-			editor.ui.focusTracker.isFocused = true;
+	describe( 'deprecated toolbar', () => {
+		let editor, editorElement, warnMock;
+
+		it( 'table.toolbar should work as table.contentToolbar', () => {
+			editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
+			warnMock = sinon.stub( window.console, 'warn' );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Paragraph, Table, TableToolbar, FakeButton ],
+					table: {
+						toolbar: [ 'fake_button' ]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
+					const toolbarView = widgetToolbarRepository._toolbars.get( 'tableContent' ).view;
+
+					expect( toolbarView.items ).to.have.length( 1 );
+					expect( toolbarView.items.get( 0 ).label ).to.equal( 'fake button' );
+
+					sinon.assert.calledWith(
+						warnMock,
+						'`config.table.toolbar` is deprecated and will be removed in the next major release.' +
+						' Use `config.table.contentToolbar` instead.'
+					);
+				} );
 		} );
 
-		it( 'should not show the toolbar on ui#update when the table is selected', () => {
-			setData( model, '<paragraph>foo</paragraph>[<table><tableRow><tableCell></tableCell></tableRow></table>]' );
+		it( 'table.contentToolbar should be used if both toolbars options are provided', () => {
+			editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
+			warnMock = sinon.stub( window.console, 'warn' );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Paragraph, Table, TableToolbar, FakeButton, FooButton ],
+					table: {
+						toolbar: [ 'fake_button' ],
+						contentToolbar: [ 'foo_button' ],
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
+					const toolbarView = widgetToolbarRepository._toolbars.get( 'tableContent' ).view;
+
+					expect( toolbarView.items ).to.have.length( 1 );
+					expect( toolbarView.items.get( 0 ).label ).to.equal( 'foo button' );
+				} );
+		} );
 
-			expect( balloon.visibleView ).to.be.null;
+		afterEach( () => {
+			editorElement.remove();
+
+			return editor.destroy();
 		} );
+	} );
 
-		it( 'should show the toolbar on ui#update when the table content is selected', () => {
-			setData(
-				model,
-				'<paragraph>[foo]</paragraph><table><tableRow><tableCell><paragraph>bar</paragraph></tableCell></tableRow></table>'
-			);
+	describe( 'tableToolbar', () => {
+		let editor, element, widgetToolbarRepository, balloon, toolbar, model;
+
+		beforeEach( () => {
+			element = document.createElement( 'div' );
+			document.body.appendChild( element );
 
-			expect( balloon.visibleView ).to.be.null;
+			return ClassicTestEditor.create( element, {
+				plugins: [ Paragraph, Table, TableToolbar, FakeButton ],
+				table: {
+					tableToolbar: [ 'fake_button' ]
+				}
+			} ).then( _editor => {
+				editor = _editor;
+				widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
+				toolbar = widgetToolbarRepository._toolbars.get( 'table' ).view;
+				balloon = editor.plugins.get( 'ContextualBalloon' );
+				model = editor.model;
+			} );
+		} );
 
-			editor.ui.fire( 'update' );
+		afterEach( () => {
+			return editor.destroy()
+				.then( () => element.remove() );
+		} );
 
-			expect( balloon.visibleView ).to.be.null;
+		describe( 'toolbar', () => {
+			it( 'should not initialize if there is no configuration', () => {
+				const editorElement = global.document.createElement( 'div' );
+				global.document.body.appendChild( editorElement );
+
+				return ClassicTestEditor.create( editorElement, {
+					plugins: [ TableToolbar ]
+				} )
+					.then( editor => {
+						const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
+						expect( widgetToolbarRepository._toolbars.get( 'table' ) ).to.be.undefined;
+
+						editorElement.remove();
+						return editor.destroy();
+					} );
+			} );
 
-			model.change( writer => {
-				// Select the <tableCell>[bar]</tableCell>
-				writer.setSelection(
-					Range.createOn( doc.getRoot().getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 ) )
-				);
+			it( 'should use the config.table.tableWidget to create items', () => {
+				expect( toolbar.items ).to.have.length( 1 );
+				expect( toolbar.items.get( 0 ).label ).to.equal( 'fake button' );
 			} );
 
-			expect( balloon.visibleView ).to.equal( toolbar );
+			it( 'should set proper CSS classes', () => {
+				const spy = sinon.spy( balloon, 'add' );
+
+				editor.ui.focusTracker.isFocused = true;
 
-			// Make sure successive change does not throw, e.g. attempting
-			// to insert the toolbar twice.
-			editor.ui.fire( 'update' );
-			expect( balloon.visibleView ).to.equal( toolbar );
+				setData( model, '[<table><tableRow><tableCell></tableCell></tableRow></table>]' );
+
+				sinon.assert.calledWithMatch( spy, {
+					view: toolbar,
+					balloonClassName: 'ck-toolbar-container'
+				} );
+			} );
 		} );
 
-		it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
-			setData( model, '<table><tableRow><tableCell><paragraph>x[y]z</paragraph></tableCell></tableRow></table>' );
+		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;
 
-			expect( balloon.visibleView ).to.equal( toolbar );
+				setData( model, '[<table><tableRow><tableCell></tableCell></tableRow></table>]' );
 
-			// Put anything on top of the ContextualBalloon stack above the table toolbar.
-			const lastView = new View();
-			lastView.element = document.createElement( 'div' );
+				editor.ui.focusTracker.isFocused = false;
+				expect( balloon.visibleView ).to.be.null;
 
-			balloon.add( {
-				view: lastView,
-				position: {
-					target: document.body
-				}
+				editor.ui.focusTracker.isFocused = true;
+				expect( balloon.visibleView ).to.equal( toolbar );
 			} );
 
-			expect( balloon.visibleView ).to.equal( lastView );
+			it( 'should hide the toolbar when the editor loses focus and the table is selected', () => {
+				editor.ui.focusTracker.isFocused = false;
 
-			editor.ui.fire( 'update' );
+				setData( model, '[<table><tableRow><tableCell></tableCell></tableRow></table>]' );
 
-			expect( balloon.visibleView ).to.equal( lastView );
+				editor.ui.focusTracker.isFocused = true;
+				expect( balloon.visibleView ).to.equal( toolbar );
+
+				editor.ui.focusTracker.isFocused = false;
+				expect( balloon.visibleView ).to.be.null;
+			} );
 		} );
 
-		it( 'should hide the toolbar on render if the table is de–selected', () => {
-			setData(
-				model,
-				'<paragraph>foo</paragraph><table><tableRow><tableCell><paragraph>[]</paragraph></tableCell></tableRow></table>'
-			);
+		describe( 'integration with the editor selection', () => {
+			beforeEach( () => {
+				editor.ui.focusTracker.isFocused = true;
+			} );
+
+			it( 'should show the toolbar on ui#update when the table widget is selected', () => {
+				setData( editor.model, '<paragraph>[foo]</paragraph><table><tableRow><tableCell></tableCell></tableRow></table>' );
 
-			expect( balloon.visibleView ).to.equal( toolbar );
+				expect( balloon.visibleView ).to.be.null;
 
-			model.change( writer => {
-				// Select the <paragraph>[...]</paragraph>
-				writer.setSelection(
-					Range.createIn( doc.getRoot().getChild( 0 ) )
-				);
+				editor.ui.fire( 'update' );
+
+				expect( balloon.visibleView ).to.be.null;
+
+				editor.model.change( writer => {
+					// Select the [<table></table>]
+					writer.setSelection( editor.model.document.getRoot().getChild( 1 ), 'on' );
+				} );
+
+				expect( balloon.visibleView ).to.equal( toolbar );
+
+				// Make sure successive change does not throw, e.g. attempting
+				// to insert the toolbar twice.
+				editor.ui.fire( 'update' );
+				expect( balloon.visibleView ).to.equal( toolbar );
 			} );
 
-			expect( balloon.visibleView ).to.be.null;
+			it( 'should not show the toolbar on ui#update when the selection is inside a table cell', () => {
+				setData( editor.model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
 
-			// Make sure successive change does not throw, e.g. attempting
-			// to remove the toolbar twice.
-			editor.ui.fire( 'update' );
-			expect( balloon.visibleView ).to.be.null;
+				expect( balloon.visibleView ).to.be.null;
+
+				editor.ui.fire( 'update' );
+
+				expect( balloon.visibleView ).to.be.null;
+			} );
+
+			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,
+					position: {
+						target: document.body
+					}
+				} );
+
+				expect( balloon.visibleView ).to.equal( lastView );
+
+				editor.ui.fire( 'update' );
+
+				expect( balloon.visibleView ).to.equal( lastView );
+			} );
+
+			it( 'should hide the toolbar on ui#update 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( model.document.getRoot().getChild( 0 ), 'in' );
+				} );
+
+				expect( balloon.visibleView ).to.be.null;
+
+				// Make sure successive change does not throw, e.g. attempting
+				// to remove the toolbar twice.
+				editor.ui.fire( 'update' );
+				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 );
+// 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'
-				} );
+			view.set( {
+				label: 'fake button'
+			} );
 
-				return view;
+			return view;
+		} );
+	}
+}
+
+// Plugin that adds foo_button to editor's component factory.
+class FooButton extends Plugin {
+	init() {
+		this.editor.ui.componentFactory.add( 'foo_button', locale => {
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: 'foo button'
 			} );
-		}
+
+			return view;
+		} );
 	}
-} );
+}

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

@@ -1 +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>
+<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M3 6v3h4V6H3zm0 4v3h4v-3H3zm0 4v3h4v-3H3zm5 3h4v-3H8v3zm5 0h4v-3h-4v3zm4-4v-3h-4v3h4zm0-4V6h-4v3h4zm1.5 8a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 17V4c.222-.863 1.068-1.5 2-1.5h13c.932 0 1.778.637 2 1.5v13zM12 13v-3H8v3h4zm0-4V6H8v3h4z" fill="#333" fill-rule="evenodd"/></svg>