Ver código fonte

Merge branch 'master' into t/ckeditor5/488

# Conflicts:
#	docs/framework/guides/external-ui.md
Szymon Cofalik 8 anos atrás
pai
commit
4134bfcbd2
21 arquivos alterados com 449 adições e 287 exclusões
  1. 151 97
      packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js
  2. 156 76
      packages/ckeditor5-ui/docs/framework/guides/external-ui.md
  3. 2 1
      packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.js
  4. 1 0
      packages/ckeditor5-ui/src/dropdown/utils.js
  5. 2 1
      packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js
  6. 18 18
      packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js
  7. 1 1
      packages/ckeditor5-ui/src/view.js
  8. 1 1
      packages/ckeditor5-ui/src/viewcollection.js
  9. 8 0
      packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js
  10. 1 1
      packages/ckeditor5-ui/tests/manual/balloontoolbar/balloontoolbar.html
  11. 5 5
      packages/ckeditor5-ui/tests/manual/balloontoolbar/balloontoolbar.js
  12. 0 0
      packages/ckeditor5-ui/tests/manual/balloontoolbar/balloontoolbar.md
  13. 3 3
      packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.js
  14. 5 5
      packages/ckeditor5-ui/tests/manual/contextualballoon/externalchanges.js
  15. 1 1
      packages/ckeditor5-ui/tests/manual/contextualballoon/externalchanges.md
  16. 5 5
      packages/ckeditor5-ui/tests/manual/tickets/198/1.js
  17. 1 1
      packages/ckeditor5-ui/tests/manual/tickets/198/1.md
  18. 5 4
      packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js
  19. 66 67
      packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js
  20. 10 0
      packages/ckeditor5-ui/theme/components/dropdown/listdropdown.css
  21. 7 0
      packages/ckeditor5-ui/theme/components/dropdown/splitbutton.css

+ 151 - 97
packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js

@@ -7,6 +7,9 @@
 
 // Basic classes to create an editor.
 import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
+import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
@@ -34,75 +37,68 @@ import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicedi
 import UnderlineEditing from '@ckeditor/ckeditor5-basic-styles/src/underline/underlineediting';
 import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
 
-// EasyImage
+// The easy image integration.
 import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
 import { TOKEN_URL } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config';
 
-// Extending the Editor, which brings base editor API.
+// Extending the Editor class, which brings base editor API.
 export default class BootstrapEditor extends Editor {
 	constructor( element, config ) {
 		super( config );
 
+		// Remember the element the editor is created with.
 		this.element = element;
 
-		// Create the ("main") root element of the model tree.
-		this.model.document.createRoot();
-
 		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
 
-		// This editor uses a single editable view in DOM.
-		this.editable = new InlineEditableUIView( this.locale );
+		// Create the ("main") root element of the model tree.
+		this.model.document.createRoot();
 
-		// A helper to easily replace the editor#element with editor.editable#element.
-		this._elementReplacer = new ElementReplacer();
+		// The UI layer of the editor.
+		this.ui = new BootstrapEditorUI( this );
 
 		// When editor#element is a textarea inside a form element
 		// then content of this textarea will be updated on form submit.
 		attachToForm( this );
+
+		// A helper to easily replace the editor#element with editor.editable#element.
+		this._elementReplacer = new ElementReplacer();
 	}
 
 	destroy() {
 		// When destroyed, editor sets the output of editor#getData() into editor#element...
 		this.updateElement();
 
-		// ...and restores editor#element.
+		// ...and restores the original editor#element...
 		this._elementReplacer.restore();
 
+		// ...and destroys the UI.
+		this.ui.destroy();
+
 		return super.destroy();
 	}
 
 	static create( element, config ) {
 		return new Promise( resolve => {
 			const editor = new this( element, config );
-			const editable = editor.editable;
+			const editable = editor.ui.view.editable;
 
 			resolve(
 				editor.initPlugins()
-					// Render the editable view in DOM first.
-					.then( () => editable.render() )
-					// Replace the editor#element with editor.editable#element.
-					.then( () => editor._elementReplacer.replace( element, editable.element ) )
-					// Handle the UI of the editor.
 					.then( () => {
-						// Create an editing root in the editing layer. It will correspond with the
-						// document root created in the constructor().
-						const editingRoot = editor.editing.createRoot( 'div' );
-
-						// Bind the basic attributes of the editable in DOM with the editing layer.
-						editable.bind( 'isReadOnly' ).to( editingRoot );
-						editable.bind( 'isFocused' ).to( editor.editing.view );
-						editable.name = editingRoot.rootName;
+						// Initialize the UI first. See the BootstrapEditorUI class to learn more.
+						editor.ui.init();
 
-						// Setup the external Bootstrap UI so it works with the editor.
-						setupButtons( editor );
-						setupHeadingDropdown( editor );
+						// Replace the editor#element with editor.editable#element.
+						editor._elementReplacer.replace( element, editable.element );
 
 						// Tell the world that the UI of the editor is ready to use.
 						editor.fire( 'uiReady' );
 					} )
 					// Bind the editor editing layer to the editable in DOM.
 					.then( () => editor.editing.view.attachDomRoot( editable.element ) )
+					// Fill the editable with the intial data.
 					.then( () => editor.loadDataFromElement() )
 					// Fire the events that announce that the editor is complete and ready to use.
 					.then( () => {
@@ -119,91 +115,149 @@ export default class BootstrapEditor extends Editor {
 mix( BootstrapEditor, DataApiMixin );
 mix( BootstrapEditor, ElementApiMixin );
 
-// This function activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
-function setupButtons( editor ) {
-	[ 'bold', 'italic', 'underline', 'undo', 'redo' ].forEach( commandName => {
-		// Retrieve the editor command corresponding with the id of the button in DOM.
-		const command = editor.commands.get( commandName );
+// The class organizing the UI of the editor, binding it with existing Bootstrap elements in DOM.
+class BootstrapEditorUI {
+	constructor( editor ) {
+		this.editor = editor;
 
-		// Retrieve the jQuery object corresponding with the button in DOM.
-		const button = $( `#${ commandName }` );
+		// The global UI view of the editor. It aggregates various Bootstrap DOM elements.
+		const view = this.view = new EditorUIView( editor.locale );
 
-		// Clicking on the buttons should execute the editor command...
-		button.click( () => editor.execute( commandName ) );
+		// This is the main editor element in DOM.
+		view.element = $( '.ck-editor' );
 
-		// ...but it should not steal the focus so the editing is uninterrupted.
-		button.mousedown( evt => evt.preventDefault() );
+		// This is the editable view in DOM. It will replace the data container in DOM.
+		view.editable = new InlineEditableUIView( editor.locale );
 
-		// Commands can become disabled, e.g. when the editor is read-only.
-		// Make sure the buttons reflect this state change.
-		command.on( 'change:isEnabled', onIsEnabledChange );
-		onIsEnabledChange();
+		// References to the dropdown elements for further usage. See #_setupBootstrapHeadingDropdown.
+		view.dropdownMenu = view.element.find( '.dropdown-menu' );
+		view.dropdownToggle = view.element.find( '.dropdown-toggle' );
 
-		// Bold, Italic and Underline commands have a value that changes
-		// when the selection starts in an element the command creates.
-		// The button should indicate that e.g. editing text which is already bold.
-		if ( !new Set( [ 'undo', 'redo' ] ).has( commandName ) ) {
-			command.on( 'change:value', onValueChange );
-			onValueChange();
-		}
+		// References to the toolbar buttons for further usage. See #_setupBootstrapToolbarButtons.
+		view.toolbarButtons = {};
 
-		function onValueChange() {
-			button.toggleClass( 'active', command.value );
-		}
+		[ 'bold', 'italic', 'underline', 'undo', 'redo' ].forEach( name => {
+			// Retrieve the jQuery object corresponding with the button in DOM.
+			view.toolbarButtons[ name ] = view.element.find( `#${ name }` );
+		} );
 
-		function onIsEnabledChange() {
-			button.attr( 'disabled', () => !command.isEnabled );
-		}
-	} );
-}
+		// Mandatory EditorUI interface components.
+		this.componentFactory = new ComponentFactory( editor );
+		this.focusTracker = new FocusTracker();
+	}
 
-// This function activates the headings dropdown in the toolbar.
-function setupHeadingDropdown( editor ) {
-	const menu = $( '.ck-editor .dropdown-menu' );
-	const button = $( '.ck-editor .dropdown-toggle' );
-
-	// Create a dropdown menu entry for each heading configuration option.
-	editor.config.get( 'heading.options' ).map( option => {
-		// Retrieve the editor command corresponding with the configuration option.
-		const command = editor.commands.get( option.modelElement );
-
-		// Create the menu item DOM element.
-		const menuItem = $(
-			`<a href="#" class="dropdown-item heading-item_${ option.modelElement }">` +
-				`${ option.title }` +
-			'</a>' );
-
-		// Upon click, the dropdown menua item should execute the command and focus
-		// the editing view to keep the editing process uninterrupted.
-		menuItem.click( () => {
-			editor.execute( option.modelElement );
-			editor.editing.view.focus();
-		} );
+	init() {
+		const editor = this.editor;
+		const view = this.view;
 
-		menu.append( menuItem );
+		// Render the editable component in DOM first.
+		view.editable.render();
 
-		// Make sure the dropdown and its items reflect the state of the
-		// currently active command.
-		command.on( 'change:value', onValueChange );
-		onValueChange();
+		// Create an editing root in the editing layer. It will correspond with the
+		// document root created in the constructor().
+		const editingRoot = editor.editing.view.document.getRoot();
 
-		// Heading commands can become disabled, e.g. when the editor is read-only.
-		// Make sure the UI reflects this state change.
-		command.on( 'change:isEnabled', onIsEnabledChange );
-		onIsEnabledChange();
+		// Bind the basic attributes of the editable in DOM with the editing layer.
+		view.editable.bind( 'isReadOnly' ).to( editingRoot );
+		view.editable.bind( 'isFocused' ).to( editor.editing.view.document );
+		view.editable.name = editingRoot.rootName;
 
-		function onValueChange() {
-			if ( command.value ) {
-				button.children( ':first' ).text( option.title );
-			}
+		// Setup the existing, external Bootstrap UI so it works with the rest of the editor.
+		this._setupBootstrapToolbarButtons();
+		this._setupBootstrapHeadingDropdown();
+	}
 
-			menuItem.toggleClass( 'active', command.value );
-		}
+	destroy() {
+		this.view.editable.destroy();
+	}
+
+	// This method activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
+	_setupBootstrapToolbarButtons() {
+		const editor = this.editor;
+
+		for ( const name in this.view.toolbarButtons ) {
+			// Retrieve the editor command corresponding with the id of the button in DOM.
+			const command = editor.commands.get( name );
+			const button = this.view.toolbarButtons[ name ];
+
+			// Clicking on the buttons should execute the editor command...
+			button.click( () => editor.execute( name ) );
 
-		function onIsEnabledChange() {
-			button.attr( 'disabled', () => !command.isEnabled );
+			// ...but it should not steal the focus so the editing is uninterrupted.
+			button.mousedown( evt => evt.preventDefault() );
+
+			const onValueChange = () => {
+				button.toggleClass( 'active', command.value );
+			};
+
+			const onIsEnabledChange = () => {
+				button.attr( 'disabled', () => !command.isEnabled );
+			};
+
+			// Commands can become disabled, e.g. when the editor is read-only.
+			// Make sure the buttons reflect this state change.
+			command.on( 'change:isEnabled', onIsEnabledChange );
+			onIsEnabledChange();
+
+			// Bold, Italic and Underline commands have a value that changes
+			// when the selection starts in an element the command creates.
+			// The button should indicate that e.g. editing text which is already bold.
+			if ( !new Set( [ 'undo', 'redo' ] ).has( name ) ) {
+				command.on( 'change:value', onValueChange );
+				onValueChange();
+			}
 		}
-	} );
+	}
+
+	// This method activates the headings dropdown in the toolbar.
+	_setupBootstrapHeadingDropdown() {
+		const editor = this.editor;
+		const dropdownMenu = this.view.dropdownMenu;
+		const dropdownToggle = this.view.dropdownToggle;
+
+		// Create a dropdown menu entry for each heading configuration option.
+		editor.config.get( 'heading.options' ).map( option => {
+			// Retrieve the editor command corresponding with the configuration option.
+			const command = editor.commands.get( option.model );
+
+			// Create the menu item DOM element.
+			const menuItem = $(
+				`<a href="#" class="dropdown-item heading-item_${ option.model }">` +
+					`${ option.title }` +
+				'</a>' );
+
+			// Upon click, the dropdown menua item should execute the command and focus
+			// the editing view to keep the editing process uninterrupted.
+			menuItem.click( () => {
+				editor.execute( option.model );
+				editor.editing.view.focus();
+			} );
+
+			dropdownMenu.append( menuItem );
+
+			// Make sure the dropdown and its items reflect the state of the
+			// currently active command.
+			command.on( 'change:value', onValueChange );
+			onValueChange();
+
+			// Heading commands can become disabled, e.g. when the editor is read-only.
+			// Make sure the UI reflects this state change.
+			command.on( 'change:isEnabled', onIsEnabledChange );
+			onIsEnabledChange();
+
+			function onValueChange() {
+				if ( command.value ) {
+					dropdownToggle.children( ':first' ).text( option.title );
+				}
+
+				menuItem.toggleClass( 'active', command.value );
+			}
+
+			function onIsEnabledChange() {
+				dropdownToggle.attr( 'disabled', () => !command.isEnabled );
+			}
+		} );
+	}
 }
 
 // Finally, create the BootstrapEditor instance with a selected set of features.

+ 156 - 76
packages/ckeditor5-ui/docs/framework/guides/external-ui.md

@@ -5,20 +5,23 @@ order: 20
 
 # Third–party UI
 
-CKEditor 5 is a modular editing framework that allows for various flexible configurations. That includes the usage of a third–party user interface on top of the basic editor classes.
+CKEditor 5 is a modular editing framework that allows various flexible configurations. That includes the usage of a third–party user interface on top of the base editor classes.
 
-In this guide, a [classic–like](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-classic) editor will be bound to a completely separate UI created in [Bootstrap](http://getbootstrap.com/), providing the basic structure and toolbar items necessary to start editing.
+In this guide, a [classic–like](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-classic) editor will be bound to a completely separate, existing UI created in [Bootstrap](http://getbootstrap.com/), providing the basic structure and toolbar items necessary to start editing.
 
 {@snippet examples/bootstrap-ui}
 
 ## Readying the editor side
 
-The ready–to–use builds of CKEditor like {@link examples/builds/classic-editor Classic} or {@link examples/builds/inline-editor Inline} come with a dedicated default user interface and theme. However, to create an editor instance bound to a Bootstrap UI, only a limited subset of features is required. You need to import them first:
+The ready–to–use builds of CKEditor like {@link examples/builds/classic-editor Classic} or {@link examples/builds/inline-editor Inline} come with a dedicated default user interface and a theme. However, to create an editor instance bound to a Bootstrap UI, only a limited subset of features is required. You need to import them first:
 
 
 ```js
 // Basic classes to create an editor.
 import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
+import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
@@ -40,11 +43,15 @@ import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoengine';
 
-// Basic features to be associated with the edited content.
+// Basic features to associated with the edited content.
 import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
 import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
 import UnderlineEditing from '@ckeditor/ckeditor5-basic-styles/src/underline/underlineediting';
 import HeadingEditing from '@ckeditor/ckeditor5-heading/src/heading/headingediting';
+
+// The easy image integration.
+import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
+import { TOKEN_URL } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config';
 ```
 
 <info-box info>
@@ -58,69 +65,64 @@ import HeadingEditing from '@ckeditor/ckeditor5-heading/src/heading/headingediti
 Having imported the very basic editor components, you can define the custom `BootstrapEditor` class that extends the {@link module:core/editor/editor~Editor `Editor`}:
 
 ```js
-// Extending the Editor, which brings base editor API.
+// Extending the Editor class, which brings base editor API.
 export default class BootstrapEditor extends Editor {
 	constructor( element, config ) {
-		super( element, config );
+		super( config );
 
-		// Create the ("main") root element of the model tree.
-		this.model.document.createRoot();
+		// Remember the element the editor is created with.
+		this.element = element;
 
 		// Use the HTML data processor in this editor.
 		this.data.processor = new HtmlDataProcessor();
 
-		// This editor uses a single editable view in DOM.
-		this.editable = new InlineEditableUIView( this.locale );
+		// Create the ("main") root element of the model tree.
+		this.model.document.createRoot();
 
-		// A helper to easily replace the editor#element with editor.editable#element.
-		this._elementReplacer = new ElementReplacer();
+		// The UI layer of the editor.
+		this.ui = new BootstrapEditorUI( this );
 
 		// When editor#element is a textarea inside a form element
 		// then content of this textarea will be updated on form submit.
 		attachToForm( this );
+
+		// A helper to easily replace the editor#element with editor.editable#element.
+		this._elementReplacer = new ElementReplacer();
 	}
 
 	destroy() {
 		// When destroyed, editor sets the output of editor#getData() into editor#element...
 		this.updateElement();
 
-		// ...and restores editor#element.
+		// ...and restores the original editor#element...
 		this._elementReplacer.restore();
 
+		// ...and destroys the UI.
+		this.ui.destroy();
+
 		return super.destroy();
 	}
 
 	static create( element, config ) {
 		return new Promise( resolve => {
 			const editor = new this( element, config );
-			const editable = editor.editable;
+			const editable = editor.ui.view.editable;
 
 			resolve(
 				editor.initPlugins()
-					// Render the editable view in DOM first.
-					.then( () => editable.render() )
-					// Replace the editor#element with editor.editable#element.
-					.then( () => editor._elementReplacer.replace( element, editable.element ) )
-					// Handle the UI of the editor.
 					.then( () => {
-						// Create an editing root in the editing layer. It will correspond to the
-						// document root created in the constructor().
-						const editingRoot = editor.editing.createRoot( 'div' );
-
-						// Bind the basic attributes of the editable in DOM with the editing layer.
-						editable.bind( 'isReadOnly' ).to( editingRoot );
-						editable.bind( 'isFocused' ).to( editor.editing.view );
-						editable.name = editingRoot.rootName;
+						// Initialize the UI first. See the BootstrapEditorUI class to learn more.
+						editor.ui.init();
 
-						// Setup the external Bootstrap UI so it works with the editor. Check out the code samples below to learn more.
-						setupButtons( editor );
-						setupHeadingDropdown( editor );
+						// Replace the editor#element with editor.editable#element.
+						editor._elementReplacer.replace( element, editable.element );
 
 						// Tell the world that the UI of the editor is ready to use.
 						editor.fire( 'uiReady' );
 					} )
 					// Bind the editor editing layer to the editable in DOM.
 					.then( () => editor.editing.view.attachDomRoot( editable.element ) )
+					// Fill the editable with the intial data.
 					.then( () => editor.loadDataFromElement() )
 					// Fire the events that announce that the editor is complete and ready to use.
 					.then( () => {
@@ -244,13 +246,90 @@ as different headings are selected. */
 
 ## Binding the UI with the editor
 
-At this stage, you are about to bind the editor created at the very beginning of this guide with the Bootstrap UI defined in HTML. Almost every feature in the editor defines some command, e.g. {@link module:heading/headingcommand~HeadingCommand} or {@link module:undo/undocommand~UndoCommand}. Commands can be executed:
+At this stage, we should bind the editor created at the very beginning of this guide with the Bootstrap UI defined in HTML. All the UI logic will be wrapped into a separate class matching the `EditorUI` {@link module:core/editor/editorui~EditorUI interface}. You may have noticed this line in the constructor of the `BootstrapEditor`:
+
+```js
+this.ui = new BootstrapEditorUI( this );
+```
+
+Let's define the `BootstrapEditorUI` then a have a closer look at the content of the class:
+
+```js
+// The class organizing the UI of the editor, binding it with existing Bootstrap elements in DOM.
+class BootstrapEditorUI {
+	constructor( editor ) {
+		this.editor = editor;
+
+		// The global UI view of the editor. It aggregates various Bootstrap DOM elements.
+		const view = this.view = new EditorUIView( editor.locale );
+
+		// This is the main editor element in DOM.
+		view.element = $( '.ck-editor' );
+
+		// This is the editable view in DOM. It will replace the data container in DOM.
+		view.editable = new InlineEditableUIView( editor.locale );
+
+		// References to the dropdown elements for further usage. See #_setupBootstrapHeadingDropdown.
+		view.dropdownMenu = view.element.find( '.dropdown-menu' );
+		view.dropdownToggle = view.element.find( '.dropdown-toggle' );
+
+		// References to the toolbar buttons for further usage. See #_setupBootstrapToolbarButtons.
+		view.toolbarButtons = {};
+
+		[ 'bold', 'italic', 'underline', 'undo', 'redo' ].forEach( name => {
+			// Retrieve the jQuery object corresponding with the button in DOM.
+			view.toolbarButtons[ name ] = view.element.find( `#${ name }` );
+		} );
+
+		// Mandatory EditorUI interface components.
+		this.componentFactory = new ComponentFactory( editor );
+		this.focusTracker = new FocusTracker();
+	}
+
+	init() {
+		const editor = this.editor;
+		const view = this.view;
+
+		// Render the editable component in DOM first.
+		view.editable.render();
+
+		// Create an editing root in the editing layer. It will correspond with the
+		// document root created in the constructor().
+		const editingRoot = editor.editing.view.document.getRoot();
+
+		// Bind the basic observable properties of the editable in DOM with the editing layer.
+		view.editable.bind( 'isReadOnly' ).to( editingRoot );
+		view.editable.bind( 'isFocused' ).to( editor.editing.view.document );
+		view.editable.name = editingRoot.rootName;
+
+		// Setup the existing, external Bootstrap UI so it works with the rest of the editor.
+		this._setupBootstrapToolbarButtons();
+		this._setupBootstrapHeadingDropdown();
+	}
+
+	destroy() {
+		this.view.editable.destroy();
+	}
+
+	// This method activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
+	_setupBootstrapToolbarButtons() {
+		...
+	}
+
+	// This method activates the headings dropdown in the toolbar.
+	_setupBootstrapHeadingDropdown() {
+		...
+	}
+}
+```
+
+Almost every feature in the editor defines some command, e.g. {@link module:heading/headingcommand~HeadingCommand} or {@link module:undo/undocommand~UndoCommand}. Commands can be executed:
 
 ```js
 editor.exectute( 'undo' );
 ```
 
-But they also come with default observable attributes like `value` and `isEnabled`. These are the entry points when it comes to creating a custom user interface because their values represent the actual state of the editor and can be followed in simple event listeners:
+But they also come with default observable properties like `value` and `isEnabled`. These are the entry points when it comes to creating a custom user interface because their values represent the actual state of the editor and can be followed in simple event listeners:
 
 ```js
 const command = editor.commands.get( 'undo' );
@@ -268,85 +347,86 @@ command.on( 'change:isEnabled', ( evt, name, isEnabled ) => {
 	To learn more about editor commands, check out the {@link module:core/command~Command} API. You can also `console.log` the {@link module:core/editor/editor~Editor#commands `editor.commands`} collection of a live editor to learn which commands it offers.
 </info-box>
 
-Now take a closer look at these two mysterious lines in the `BootstrapEditor#create()` method:
+Knowing that, let's fill out the missing methods of the `BootstrapEditorUI`.
 
-```js
-setupButtons( editor );
-setupHeadingDropdown( editor );
-```
+### Binding the buttons to editor commands
 
-`setupButtons()` is a function that binds Bootstrap toolbar buttons with the editor features. It activates the related editor commands upon clicking and makes the buttons listen to the state of the commands to update their CSS classes:
+`_setupBootstrapToolbarButtons()` is a method that binds Bootstrap toolbar buttons to the editor features (commands). It activates the corresponding editor commands upon clicking and makes the buttons listen to the state of the commands to update their CSS classes:
 
 ```js
-// This function activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
-function setupButtons( editor ) {
-	[ 'bold', 'italic', 'underline', 'undo', 'redo' ].forEach( commandName => {
-		// Retrieve the editor command corresponding with the ID of the button in DOM.
-		const command = editor.commands.get( commandName );
+// This method activates Bold, Italic, Underline, Undo and Redo buttons in the toolbar.
+_setupBootstrapToolbarButtons() {
+	const editor = this.editor;
 
-		// Retrieve the jQuery object corresponding with the button in DOM.
-		const button = $( `#${ commandName }` );
+	for ( const name in this.view.toolbarButtons ) {
+		// Retrieve the editor command corresponding with the id of the button in DOM.
+		const command = editor.commands.get( name );
+		const button = this.view.toolbarButtons[ name ];
 
-		// Clicking the buttons should execute the editor command...
-		button.click( () => editor.execute( commandName ) );
+		// Clicking on the buttons should execute the editor command...
+		button.click( () => editor.execute( name ) );
 
 		// ...but it should not steal the focus so the editing is uninterrupted.
 		button.mousedown( evt => evt.preventDefault() );
 
+		const onValueChange = () => {
+			button.toggleClass( 'active', command.value );
+		};
+
+		const onIsEnabledChange = () => {
+			button.attr( 'disabled', () => !command.isEnabled );
+		};
+
 		// Commands can become disabled, e.g. when the editor is read-only.
 		// Make sure the buttons reflect this state change.
 		command.on( 'change:isEnabled', onIsEnabledChange );
 		onIsEnabledChange();
 
 		// Bold, Italic and Underline commands have a value that changes
-		// when the selection starts in an element that the command creates.
-		// The button should indicate that the user is editing a text which
-		// is already bold.
-		if ( !new Set( [ 'undo', 'redo' ] ).has( commandName ) ) {
+		// when the selection starts in an element the command creates.
+		// The button should indicate that e.g. editing text which is already bold.
+		if ( !new Set( [ 'undo', 'redo' ] ).has( name ) ) {
 			command.on( 'change:value', onValueChange );
 			onValueChange();
 		}
-
-		function onValueChange() {
-			button.toggleClass( 'active', command.value );
-		}
-
-		function onIsEnabledChange() {
-			button.attr( 'disabled', () => !command.isEnabled );
-		}
-	} );
+	}
 }
 ```
 
-The drop-down in the toolbar is a more complex case because first it must be populated with heading options for the users to select from. Then, clicking each option must execute a related heading command in the editor. Finally, the drop-down button and the drop-down menu items must reflect the state of the editor, for example, when the selection lands in a heading, a proper menu item should become active and the button should show the name of the heading level.
+### Binding the drop-down to the heading commands
+
+The drop-down in the toolbar is a more complex case.
+
+First, it must be populated with heading options for the users to select from. Then, clicking each option must execute a related heading command in the editor. Finally, the drop-down button and the drop-down menu items must reflect the state of the editor, for example, when the selection lands in a heading, a proper menu item should become active and the button should show the name of the heading level.
 
 ```js
-// This function activates the headings drop-down in the toolbar.
-function setupHeadingDropdown( editor ) {
-	const menu = $( '.ck-editor .dropdown-menu' );
-	const button = $( '.ck-editor .dropdown-toggle' );
+// This method activates the headings dropdown in the toolbar.
+_setupBootstrapHeadingDropdown() {
+	const editor = this.editor;
+	const dropdownMenu = this.view.dropdownMenu;
+	const dropdownToggle = this.view.dropdownToggle;
 
-	// Create a drop-down menu entry for each heading configuration option.
+	// Create a dropdown menu entry for each heading configuration option.
 	editor.config.get( 'heading.options' ).map( option => {
 		// Retrieve the editor command corresponding with the configuration option.
-		const command = editor.commands.get( option.modelElement );
-		// Create the menu item DOM element.
+		const command = editor.commands.get( option.model );
 
+		// Create the menu item DOM element.
 		const menuItem = $(
-			`<a href="#" class="dropdown-item heading-item_${ option.modelElement }">` +
+			`<a href="#" class="dropdown-item heading-item_${ option.model }">` +
 				`${ option.title }` +
 			'</a>' );
 
-		// Upon clicking, the drop-down menu item should execute the command and focus
+		// Upon click, the dropdown menua item should execute the command and focus
 		// the editing view to keep the editing process uninterrupted.
 		menuItem.click( () => {
-			editor.execute( option.modelElement );
+			editor.execute( option.model );
 			editor.editing.view.focus();
 		} );
 
-		menu.append( menuItem );
+		dropdownMenu.append( menuItem );
 
-		// Make sure the drop-down and its items reflect the state of the
+		// Make sure the dropdown and its items reflect the state of the
 		// currently active command.
 		command.on( 'change:value', onValueChange );
 		onValueChange();
@@ -358,14 +438,14 @@ function setupHeadingDropdown( editor ) {
 
 		function onValueChange() {
 			if ( command.value ) {
-				button.children( ':first' ).text( option.title );
+				dropdownToggle.children( ':first' ).text( option.title );
 			}
 
 			menuItem.toggleClass( 'active', command.value );
 		}
 
 		function onIsEnabledChange() {
-			button.attr( 'disabled', () => !command.isEnabled );
+			dropdownToggle.attr( 'disabled', () => !command.isEnabled );
 		}
 	} );
 }
@@ -373,7 +453,7 @@ function setupHeadingDropdown( editor ) {
 
 ## Running the editor
 
-When the editor class and the user interface are ready, it is time to run the editor. Just make sure all the plugins are loaded and the right DOM element is passed to `BootstrapEditor#create`:
+When the editor classes and the user interface are ready, it is time to run the editor. Just make sure all the plugins are loaded and the right DOM element is passed to `BootstrapEditor#create`:
 
 ```js
 BootstrapEditor

+ 2 - 1
packages/ckeditor5-ui/src/dropdown/button/splitbuttonview.js

@@ -109,7 +109,8 @@ export default class SplitButtonView extends View {
 			attributes: {
 				class: [
 					'ck-splitbutton',
-					bind.if( 'isVisible', 'ck-hidden', value => !value )
+					bind.if( 'isVisible', 'ck-hidden', value => !value ),
+					this.arrowView.bindTemplate.if( 'isOn', 'ck-splitbutton_open' )
 				]
 			},
 

+ 1 - 0
packages/ckeditor5-ui/src/dropdown/utils.js

@@ -17,6 +17,7 @@ import ListItemView from '../list/listitemview';
 import clickOutsideHandler from '../bindings/clickoutsidehandler';
 
 import '../../theme/components/dropdown/toolbardropdown.css';
+import '../../theme/components/dropdown/listdropdown.css';
 
 /**
  * A helper for creating dropdowns. It creates an instance of a {@link module:ui/dropdown/dropdownview~DropdownView dropdown},

+ 2 - 1
packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js

@@ -61,7 +61,8 @@ export default class ContextualBalloon extends Plugin {
 		 */
 		this.positionLimiter = () => {
 			const view = this.editor.editing.view;
-			const editableElement = view.selection.editableElement;
+			const viewDocument = view.document;
+			const editableElement = viewDocument.selection.editableElement;
 
 			if ( editableElement ) {
 				return view.domConverter.mapViewToDom( editableElement.root );

+ 18 - 18
packages/ckeditor5-ui/src/toolbar/contextual/contextualtoolbar.js → packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module ui/toolbar/contextual/contextualtoolbar
+ * @module ui/toolbar/balloon/balloontoolbar
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -22,12 +22,12 @@ import normalizeToolbarConfig from '../normalizetoolbarconfig';
  *
  * @extends module:core/plugin~Plugin
  */
-export default class ContextualToolbar extends Plugin {
+export default class BalloonToolbar extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
 	static get pluginName() {
-		return 'ContextualToolbar';
+		return 'BalloonToolbar';
 	}
 
 	/**
@@ -53,7 +53,6 @@ export default class ContextualToolbar extends Plugin {
 		this.toolbarView.extendTemplate( {
 			attributes: {
 				class: [
-					'ck-editor-toolbar',
 					'ck-toolbar_floating'
 				]
 			}
@@ -84,7 +83,7 @@ export default class ContextualToolbar extends Plugin {
 		this._handleSelectionChange();
 		this._handleFocusChange();
 
-		// The appearance of the ContextualToolbar method is event–driven.
+		// The appearance of the BalloonToolbar method is event–driven.
 		// It is possible to stop the #show event and this prevent the toolbar from showing up.
 		this.decorate( 'show' );
 	}
@@ -96,7 +95,7 @@ export default class ContextualToolbar extends Plugin {
 	 * @inheritDoc
 	 */
 	afterInit() {
-		const config = normalizeToolbarConfig( this.editor.config.get( 'contextualToolbar' ) );
+		const config = normalizeToolbarConfig( this.editor.config.get( 'balloonToolbar' ) );
 		const factory = this.editor.ui.componentFactory;
 
 		this.toolbarView.fillFromConfig( config.items, factory );
@@ -129,7 +128,7 @@ export default class ContextualToolbar extends Plugin {
 	 */
 	_handleSelectionChange() {
 		const selection = this.editor.model.document.selection;
-		const editingView = this.editor.editing.view;
+		const viewDocument = this.editor.editing.view.document;
 
 		this.listenTo( selection, 'change:range', ( evt, data ) => {
 			// When the selection is not changed by a collaboration and when is not collapsed.
@@ -145,7 +144,7 @@ export default class ContextualToolbar extends Plugin {
 		// Hide the toolbar when the selection stops changing.
 		this.listenTo( this, '_selectionChangeDebounced', () => {
 			// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
-			if ( editingView.isFocused && !editingView.selection.isCollapsed ) {
+			if ( viewDocument.isFocused && !viewDocument.selection.isCollapsed ) {
 				this.show();
 			}
 		} );
@@ -168,7 +167,7 @@ export default class ContextualToolbar extends Plugin {
 			return;
 		}
 
-		// Update the toolbar position upon #render (e.g. external document changes)
+		// Update the toolbar position upon change (e.g. external document changes)
 		// while it's visible.
 		this.listenTo( this.editor.editing.view, 'render', () => {
 			this._balloon.updatePosition( this._getBalloonPositionData() );
@@ -178,7 +177,7 @@ export default class ContextualToolbar extends Plugin {
 		this._balloon.add( {
 			view: this.toolbarView,
 			position: this._getBalloonPositionData(),
-			balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container'
+			balloonClassName: 'ck-toolbar-container'
 		} );
 	}
 
@@ -201,10 +200,11 @@ export default class ContextualToolbar extends Plugin {
 	 */
 	_getBalloonPositionData() {
 		const editor = this.editor;
-		const editingView = editor.editing.view;
+		const view = editor.editing.view;
+		const viewDocument = view.document;
 
 		// Get direction of the selection.
-		const isBackward = editingView.selection.isBackward;
+		const isBackward = viewDocument.selection.isBackward;
 
 		return {
 			// Because the target for BalloonPanelView is a Rect (not DOMRange), it's geometry will stay fixed
@@ -212,8 +212,8 @@ export default class ContextualToolbar extends Plugin {
 			// computed and hence, the target is defined as a function instead of a static value.
 			// https://github.com/ckeditor/ckeditor5-ui/issues/195
 			target: () => {
-				const range = editingView.selection.getFirstRange();
-				const rangeRects = Rect.getDomRangeRects( editingView.domConverter.viewRangeToDom( range ) );
+				const range = viewDocument.selection.getFirstRange();
+				const rangeRects = Rect.getDomRangeRects( view.domConverter.viewRangeToDom( range ) );
 
 				// Select the proper range rect depending on the direction of the selection.
 				if ( isBackward ) {
@@ -283,20 +283,20 @@ function getBalloonPositions( isBackward ) {
 }
 
 /**
- * Contextual toolbar configuration. Used by the {@link module:ui/toolbar/contextual/contextualtoolbar~ContextualToolbar}
+ * Contextual toolbar configuration. Used by the {@link module:ui/toolbar/balloon/balloontoolbar~BalloonToolbar}
  * feature.
  *
  *		const config = {
- *			contextualToolbar: [ 'bold', 'italic', 'undo', 'redo' ]
+ *			balloonToolbar: [ 'bold', 'italic', 'undo', 'redo' ]
  *		};
  *
  * You can also use `'|'` to create a separator between groups of items:
  *
  *		const config = {
- *			contextualToolbar: [ 'bold', 'italic', | 'undo', 'redo' ]
+ *			balloonToolbar: [ 'bold', 'italic', | 'undo', 'redo' ]
  *		};
  *
  * Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
  *
- * @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#contextualToolbar
+ * @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#balloonToolbar
  */

+ 1 - 1
packages/ckeditor5-ui/src/view.js

@@ -184,7 +184,7 @@ export default class View {
 		 */
 
 		/**
-		 * Cached {@link @link module:ui/template~BindChain bind chain} object created by the
+		 * Cached {@link module:ui/template~BindChain bind chain} object created by the
 		 * {@link #template}. See {@link #bindTemplate}.
 		 *
 		 * @private

+ 1 - 1
packages/ckeditor5-ui/src/viewcollection.js

@@ -53,7 +53,7 @@ export default class ViewCollection extends Collection {
 	/**
 	 * Creates a new instance of the {@link module:ui/viewcollection~ViewCollection}.
 	 *
-	 * @param {module:utils/locale~Locale} [locale] The {@link module:core/editor~Editor editor's locale} instance.
+	 * @param {module:utils/locale~Locale} [locale] The {@link module:core/editor/editor~Editor editor's locale} instance.
 	 */
 	constructor( locale ) {
 		super( {

+ 8 - 0
packages/ckeditor5-ui/tests/dropdown/button/splitbuttonview.js

@@ -52,6 +52,14 @@ describe( 'SplitButtonView', () => {
 			expect( view.actionView.element.classList.contains( 'ck-hidden' ) ).to.be.false;
 		} );
 
+		it( 'binds arrowView#isOn to the template', () => {
+			view.arrowView.isOn = true;
+			expect( view.element.classList.contains( 'ck-splitbutton_open' ) ).to.be.true;
+
+			view.arrowView.isOn = false;
+			expect( view.element.classList.contains( 'ck-splitbutton_open' ) ).to.be.false;
+		} );
+
 		describe( 'activates keyboard navigation for the toolbar', () => {
 			it( 'so "arrowright" on view#arrowView does nothing', () => {
 				const keyEvtData = {

+ 1 - 1
packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.html → packages/ckeditor5-ui/tests/manual/balloontoolbar/balloontoolbar.html

@@ -1,5 +1,5 @@
 <div id="editor">
-	<p><em>ContextualToolbar</em> won't show for the first block element.</p>
+	<p><em>BalloonToolbar</em> won't show for the first block element.</p>
 	<p><em>This</em> is a <strong>first line</strong> of text.</p>
 	<p><em>This</em> is a <strong>second line</strong> of text.</p>
 	<p><em>This</em> is the <strong>end</strong> of text.</p>

+ 5 - 5
packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.js → packages/ckeditor5-ui/tests/manual/balloontoolbar/balloontoolbar.js

@@ -7,21 +7,21 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import ContextualToolbar from '../../../src/toolbar/contextual/contextualtoolbar';
+import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, ContextualToolbar ],
+		plugins: [ ArticlePluginSet, BalloonToolbar ],
 		toolbar: [ 'bold', 'italic', 'link', 'undo', 'redo' ],
-		contextualToolbar: [ 'bold', 'italic', 'link' ]
+		balloonToolbar: [ 'bold', 'italic', 'link' ]
 	} )
 	.then( editor => {
 		window.editor = editor;
 
-		const contextualToolbar = editor.plugins.get( 'ContextualToolbar' );
+		const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
 
-		contextualToolbar.on( 'show', evt => {
+		balloonToolbar.on( 'show', evt => {
 			const selectionRange = editor.model.document.selection.getFirstRange();
 			const blockRange = Range.createOn( editor.model.document.getRoot().getChild( 0 ) );
 

+ 0 - 0
packages/ckeditor5-ui/tests/manual/contextualtoolbar/contextualtoolbar.md → packages/ckeditor5-ui/tests/manual/balloontoolbar/balloontoolbar.md


+ 3 - 3
packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.js

@@ -7,14 +7,14 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import ContextualToolbar from '../../../src/toolbar/contextual/contextualtoolbar';
+import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
 
 // Finally the editor.
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, ContextualToolbar ],
+		plugins: [ ArticlePluginSet, BalloonToolbar ],
 		toolbar: [ 'bold', 'link' ],
-		contextualToolbar: [ 'bold', 'link' ]
+		balloonToolbar: [ 'bold', 'link' ]
 	} )
 	.then( editor => {
 		window.editor = editor;

+ 5 - 5
packages/ckeditor5-ui/tests/manual/contextualballoon/externalchanges.js

@@ -6,7 +6,7 @@
 /* globals console:false, document, setTimeout */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-import ContextualToolbar from '../../../src/toolbar/contextual/contextualtoolbar';
+import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
@@ -15,9 +15,9 @@ import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 // Editor for the external insert.
 ClassicEditor
 	.create( document.querySelector( '#editor-insert' ), {
-		plugins: [ ArticlePluginSet, ContextualToolbar ],
+		plugins: [ ArticlePluginSet, BalloonToolbar ],
 		toolbar: [ 'bold', 'link' ],
-		contextualToolbar: [ 'bold', 'link' ]
+		balloonToolbar: [ 'bold', 'link' ]
 	} )
 	.then( editor => {
 		const element = document.querySelector( '#button-insert' );
@@ -32,9 +32,9 @@ ClassicEditor
 // Editor for the external delete.
 ClassicEditor
 	.create( document.querySelector( '#editor-delete' ), {
-		plugins: [ ArticlePluginSet, ContextualToolbar ],
+		plugins: [ ArticlePluginSet, BalloonToolbar ],
 		toolbar: [ 'bold', 'link' ],
-		contextualToolbar: [ 'bold', 'link' ]
+		balloonToolbar: [ 'bold', 'link' ]
 	} )
 	.then( editor => {
 		const element = document.querySelector( '#button-delete' );

+ 1 - 1
packages/ckeditor5-ui/tests/manual/contextualballoon/externalchanges.md

@@ -1,4 +1,4 @@
-## Link panel opened from ContextualToolbar should be updated on external changes.
+## Link panel opened from BalloonToolbar should be updated on external changes.
 
 ### Create #1:
 

+ 5 - 5
packages/ckeditor5-ui/tests/manual/tickets/198/1.js

@@ -7,7 +7,7 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import ContextualToolbar from '../../../../src/toolbar/contextual/contextualtoolbar';
+import BalloonToolbar from '../../../../src/toolbar/balloon/balloontoolbar';
 
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
@@ -15,9 +15,9 @@ import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 // Editor for the external insert.
 ClassicEditor
 	.create( document.querySelector( '#editor-insert' ), {
-		plugins: [ ArticlePluginSet, ContextualToolbar ],
+		plugins: [ ArticlePluginSet, BalloonToolbar ],
 		toolbar: [ 'undo', 'redo' ],
-		contextualToolbar: [ 'bold', 'italic' ]
+		balloonToolbar: [ 'bold', 'italic' ]
 	} )
 	.then( editor => {
 		const element = document.querySelector( '#button-insert' );
@@ -32,9 +32,9 @@ ClassicEditor
 // Editor for the external delete.
 ClassicEditor
 	.create( document.querySelector( '#editor-delete' ), {
-		plugins: [ ArticlePluginSet, ContextualToolbar ],
+		plugins: [ ArticlePluginSet, BalloonToolbar ],
 		toolbar: [ 'undo', 'redo' ],
-		contextualToolbar: [ 'bold', 'italic' ]
+		balloonToolbar: [ 'bold', 'italic' ]
 	} )
 	.then( editor => {
 		const element = document.querySelector( '#button-delete' );

+ 1 - 1
packages/ckeditor5-ui/tests/manual/tickets/198/1.md

@@ -1,6 +1,6 @@
 # Issue [#198](https://github.com/ckeditor/ckeditor5-ui/issues/198) manual test.
 
-## Position of the `ContextualToolbar` should be updated on external changes.
+## Position of the `BalloonToolbar` should be updated on external changes.
 
 ### Insert:
 

+ 5 - 4
packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js

@@ -74,18 +74,19 @@ describe( 'ContextualBalloon', () => {
 		} );
 
 		describe( 'positionLimiter', () => {
-			let model, viewDocument, root;
+			let model, view, viewDocument, root;
 
 			beforeEach( () => {
 				model = editor.model;
-				viewDocument = editor.editing.view;
+				view = editor.editing.view;
+				viewDocument = view.document;
 				root = viewDocument.getRoot();
 			} );
 
 			it( 'obtains the root of the selection', () => {
 				setModelData( model, '<paragraph>[]bar</paragraph>' );
 
-				expect( balloon.positionLimiter() ).to.equal( viewDocument.domConverter.mapViewToDom( root ) );
+				expect( balloon.positionLimiter() ).to.equal( view.domConverter.mapViewToDom( root ) );
 			} );
 
 			it( 'does not fail if selection has no #editableElement', () => {
@@ -114,7 +115,7 @@ describe( 'ContextualBalloon', () => {
 
 				setModelData( model, '<widget><nestedEditable>[]foo</nestedEditable></widget>' );
 
-				expect( balloon.positionLimiter() ).to.equal( viewDocument.domConverter.mapViewToDom( root ) );
+				expect( balloon.positionLimiter() ).to.equal( view.domConverter.mapViewToDom( root ) );
 			} );
 		} );
 

+ 66 - 67
packages/ckeditor5-ui/tests/toolbar/contextual/contextualtoolbar.js → packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js

@@ -4,7 +4,7 @@
  */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-import ContextualToolbar from '../../../src/toolbar/contextual/contextualtoolbar';
+import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
 import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
 import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
 import ToolbarView from '../../../src/toolbar/toolbarview';
@@ -18,8 +18,8 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js';
 
 /* global document, setTimeout, window */
 
-describe( 'ContextualToolbar', () => {
-	let sandbox, editor, model, selection, editingView, contextualToolbar, balloon, editorElement;
+describe( 'BalloonToolbar', () => {
+	let sandbox, editor, model, selection, editingView, balloonToolbar, balloon, editorElement;
 
 	beforeEach( () => {
 		sandbox = sinon.sandbox.create();
@@ -29,15 +29,15 @@ describe( 'ContextualToolbar', () => {
 
 		return ClassicTestEditor
 			.create( editorElement, {
-				plugins: [ Paragraph, Bold, Italic, ContextualToolbar ],
-				contextualToolbar: [ 'bold', 'italic' ]
+				plugins: [ Paragraph, Bold, Italic, BalloonToolbar ],
+				balloonToolbar: [ 'bold', 'italic' ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
 				model = editor.model;
 				editingView = editor.editing.view;
 				selection = model.document.selection;
-				contextualToolbar = editor.plugins.get( ContextualToolbar );
+				balloonToolbar = editor.plugins.get( BalloonToolbar );
 				balloon = editor.plugins.get( ContextualBalloon );
 
 				editingView.attachDomRoot( editorElement );
@@ -47,7 +47,7 @@ describe( 'ContextualToolbar', () => {
 				sandbox.stub( balloon.view, 'pin' ).returns( {} );
 
 				// Focus the engine.
-				editingView.isFocused = true;
+				editingView.document.isFocused = true;
 				editingView.getDomRoot().focus();
 
 				// Remove all selection ranges from DOM before testing.
@@ -63,11 +63,10 @@ describe( 'ContextualToolbar', () => {
 	} );
 
 	it( 'should create a plugin instance', () => {
-		expect( contextualToolbar ).to.instanceOf( Plugin );
-		expect( contextualToolbar ).to.instanceOf( ContextualToolbar );
-		expect( contextualToolbar.toolbarView ).to.instanceof( ToolbarView );
-		expect( contextualToolbar.toolbarView.element.classList.contains( 'ck-editor-toolbar' ) ).to.be.true;
-		expect( contextualToolbar.toolbarView.element.classList.contains( 'ck-toolbar_floating' ) ).to.be.true;
+		expect( balloonToolbar ).to.instanceOf( Plugin );
+		expect( balloonToolbar ).to.instanceOf( BalloonToolbar );
+		expect( balloonToolbar.toolbarView ).to.instanceof( ToolbarView );
+		expect( balloonToolbar.toolbarView.element.classList.contains( 'ck-toolbar_floating' ) ).to.be.true;
 	} );
 
 	it( 'should load ContextualBalloon', () => {
@@ -75,7 +74,7 @@ describe( 'ContextualToolbar', () => {
 	} );
 
 	it( 'should create components from config', () => {
-		expect( contextualToolbar.toolbarView.items ).to.length( 2 );
+		expect( balloonToolbar.toolbarView.items ).to.length( 2 );
 	} );
 
 	it( 'should accept the extended format of the toolbar config', () => {
@@ -84,15 +83,15 @@ describe( 'ContextualToolbar', () => {
 
 		return ClassicTestEditor
 			.create( editorElement, {
-				plugins: [ Paragraph, Bold, Italic, Underline, ContextualToolbar ],
-				contextualToolbar: {
+				plugins: [ Paragraph, Bold, Italic, Underline, BalloonToolbar ],
+				balloonToolbar: {
 					items: [ 'bold', 'italic', 'underline' ]
 				}
 			} )
 			.then( editor => {
-				const contextualToolbar = editor.plugins.get( ContextualToolbar );
+				const balloonToolbar = editor.plugins.get( BalloonToolbar );
 
-				expect( contextualToolbar.toolbarView.items ).to.length( 3 );
+				expect( balloonToolbar.toolbarView.items ).to.length( 3 );
 
 				editorElement.remove();
 
@@ -107,7 +106,7 @@ describe( 'ContextualToolbar', () => {
 
 		const spy = sandbox.spy();
 		setData( model, '<paragraph>[bar]</paragraph>' );
-		contextualToolbar.on( '_selectionChangeDebounced', spy );
+		balloonToolbar.on( '_selectionChangeDebounced', spy );
 
 		selection.fire( 'change:range', {} );
 
@@ -139,7 +138,7 @@ describe( 'ContextualToolbar', () => {
 
 	describe( 'pluginName', () => {
 		it( 'should return plugin by its name', () => {
-			expect( editor.plugins.get( 'ContextualToolbar' ) ).to.equal( contextualToolbar );
+			expect( editor.plugins.get( 'BalloonToolbar' ) ).to.equal( balloonToolbar );
 		} );
 	} );
 
@@ -171,7 +170,7 @@ describe( 'ContextualToolbar', () => {
 			] );
 
 			balloonAddSpy = sandbox.spy( balloon, 'add' );
-			editingView.isFocused = true;
+			editingView.document.isFocused = true;
 		} );
 
 		it( 'should add #toolbarView to the #_balloon and attach the #_balloon to the selection for the forward selection', () => {
@@ -179,11 +178,11 @@ describe( 'ContextualToolbar', () => {
 
 			const defaultPositions = BalloonPanelView.defaultPositions;
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 
 			sinon.assert.calledWith( balloonAddSpy, {
-				view: contextualToolbar.toolbarView,
-				balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container',
+				view: balloonToolbar.toolbarView,
+				balloonClassName: 'ck-toolbar-container',
 				position: {
 					target: sinon.match.func,
 					positions: [
@@ -213,7 +212,7 @@ describe( 'ContextualToolbar', () => {
 
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 			expect( balloonAddSpy.firstCall.args[ 0 ].position.target() ).to.deep.equal( forwardSelectionRect );
 		} );
 
@@ -222,11 +221,11 @@ describe( 'ContextualToolbar', () => {
 
 			const defaultPositions = BalloonPanelView.defaultPositions;
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 
 			sinon.assert.calledWithExactly( balloonAddSpy, {
-				view: contextualToolbar.toolbarView,
-				balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container',
+				view: balloonToolbar.toolbarView,
+				balloonClassName: 'ck-toolbar-container',
 				position: {
 					target: sinon.match.func,
 					positions: [
@@ -243,14 +242,14 @@ describe( 'ContextualToolbar', () => {
 			expect( balloonAddSpy.firstCall.args[ 0 ].position.target() ).to.deep.equal( backwardSelectionRect );
 		} );
 
-		it( 'should update balloon position on ViewDocument#render event while balloon is added to the #_balloon', () => {
+		it( 'should update balloon position on view#change event while balloon is added to the #_balloon', () => {
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
 			const spy = sandbox.spy( balloon, 'updatePosition' );
 
 			editingView.fire( 'render' );
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 			sinon.assert.notCalled( spy );
 
 			editingView.fire( 'render' );
@@ -260,31 +259,31 @@ describe( 'ContextualToolbar', () => {
 		it( 'should not add #toolbarView to the #_balloon more than once', () => {
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
-			contextualToolbar.show();
-			contextualToolbar.show();
+			balloonToolbar.show();
+			balloonToolbar.show();
 			sinon.assert.calledOnce( balloonAddSpy );
 		} );
 
 		it( 'should not add #toolbarView to the #_balloon when all components inside #toolbarView are disabled', () => {
-			Array.from( contextualToolbar.toolbarView.items ).forEach( item => {
+			Array.from( balloonToolbar.toolbarView.items ).forEach( item => {
 				item.isEnabled = false;
 			} );
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 			sinon.assert.notCalled( balloonAddSpy );
 		} );
 
 		it( 'should add #toolbarView to the #_balloon when at least one component inside does not have #isEnabled interface', () => {
-			Array.from( contextualToolbar.toolbarView.items ).forEach( item => {
+			Array.from( balloonToolbar.toolbarView.items ).forEach( item => {
 				item.isEnabled = false;
 			} );
 
-			delete contextualToolbar.toolbarView.items.get( 0 ).isEnabled;
+			delete balloonToolbar.toolbarView.items.get( 0 ).isEnabled;
 
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 			sinon.assert.calledOnce( balloonAddSpy );
 		} );
 
@@ -292,29 +291,29 @@ describe( 'ContextualToolbar', () => {
 			let showSpy;
 
 			beforeEach( () => {
-				showSpy = sandbox.spy( contextualToolbar, 'show' );
+				showSpy = sandbox.spy( balloonToolbar, 'show' );
 			} );
 
 			it( 'should not be called when the editor is not focused', () => {
 				setData( model, '<paragraph>b[a]r</paragraph>' );
-				editingView.isFocused = false;
+				editingView.document.isFocused = false;
 
-				contextualToolbar.fire( '_selectionChangeDebounced' );
+				balloonToolbar.fire( '_selectionChangeDebounced' );
 				sinon.assert.notCalled( showSpy );
 			} );
 
 			it( 'should not be called when the selection is collapsed', () => {
 				setData( model, '<paragraph>b[]ar</paragraph>' );
 
-				contextualToolbar.fire( '_selectionChangeDebounced' );
+				balloonToolbar.fire( '_selectionChangeDebounced' );
 				sinon.assert.notCalled( showSpy );
 			} );
 
 			it( 'should be called when the selection is not collapsed and editor is focused', () => {
 				setData( model, '<paragraph>b[a]r</paragraph>' );
-				editingView.isFocused = true;
+				editingView.document.isFocused = true;
 
-				contextualToolbar.fire( '_selectionChangeDebounced' );
+				balloonToolbar.fire( '_selectionChangeDebounced' );
 				sinon.assert.calledOnce( showSpy );
 			} );
 		} );
@@ -325,32 +324,32 @@ describe( 'ContextualToolbar', () => {
 
 		beforeEach( () => {
 			removeBalloonSpy = sandbox.stub( balloon, 'remove' ).returns( {} );
-			editingView.isFocused = true;
+			editingView.document.isFocused = true;
 		} );
 
 		it( 'should remove #toolbarView from the #_balloon', () => {
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 
-			contextualToolbar.hide();
-			sinon.assert.calledWithExactly( removeBalloonSpy, contextualToolbar.toolbarView );
+			balloonToolbar.hide();
+			sinon.assert.calledWithExactly( removeBalloonSpy, balloonToolbar.toolbarView );
 		} );
 
-		it( 'should stop update balloon position on ViewDocument#render event', () => {
+		it( 'should stop update balloon position on ViewDocument#change event', () => {
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
 			const spy = sandbox.spy( balloon, 'updatePosition' );
 
-			contextualToolbar.show();
-			contextualToolbar.hide();
+			balloonToolbar.show();
+			balloonToolbar.hide();
 
 			editingView.fire( 'render' );
 			sinon.assert.notCalled( spy );
 		} );
 
 		it( 'should not remove #ttolbarView when is not added to the #_balloon', () => {
-			contextualToolbar.hide();
+			balloonToolbar.hide();
 
 			sinon.assert.notCalled( removeBalloonSpy );
 		} );
@@ -359,19 +358,19 @@ describe( 'ContextualToolbar', () => {
 	describe( 'destroy()', () => {
 		it( 'can be called multiple times', () => {
 			expect( () => {
-				contextualToolbar.destroy();
-				contextualToolbar.destroy();
+				balloonToolbar.destroy();
+				balloonToolbar.destroy();
 			} ).to.not.throw();
 		} );
 
 		it( 'should not fire `_selectionChangeDebounced` after plugin destroy', done => {
 			const spy = sandbox.spy();
 
-			contextualToolbar.on( '_selectionChangeDebounced', spy );
+			balloonToolbar.on( '_selectionChangeDebounced', spy );
 
 			selection.fire( 'change:range', { directChange: true } );
 
-			contextualToolbar.destroy();
+			balloonToolbar.destroy();
 
 			setTimeout( () => {
 				sinon.assert.notCalled( spy );
@@ -386,22 +385,22 @@ describe( 'ContextualToolbar', () => {
 		beforeEach( () => {
 			setData( model, '<paragraph>[bar]</paragraph>' );
 
-			showPanelSpy = sandbox.spy( contextualToolbar, 'show' );
-			hidePanelSpy = sandbox.spy( contextualToolbar, 'hide' );
+			showPanelSpy = sandbox.spy( balloonToolbar, 'show' );
+			hidePanelSpy = sandbox.spy( balloonToolbar, 'hide' );
 		} );
 
 		it( 'should open when selection stops changing', () => {
 			sinon.assert.notCalled( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
 
-			contextualToolbar.fire( '_selectionChangeDebounced' );
+			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
 		} );
 
 		it( 'should close when selection starts changing by a directChange', () => {
-			contextualToolbar.fire( '_selectionChangeDebounced' );
+			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
@@ -413,7 +412,7 @@ describe( 'ContextualToolbar', () => {
 		} );
 
 		it( 'should not close when selection starts changing by not a directChange', () => {
-			contextualToolbar.fire( '_selectionChangeDebounced' );
+			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
@@ -425,7 +424,7 @@ describe( 'ContextualToolbar', () => {
 		} );
 
 		it( 'should close when selection starts changing by not a directChange but will become collapsed', () => {
-			contextualToolbar.fire( '_selectionChangeDebounced' );
+			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
@@ -443,9 +442,9 @@ describe( 'ContextualToolbar', () => {
 		it( 'should hide if the editor loses focus', () => {
 			editor.ui.focusTracker.isFocused = true;
 
-			contextualToolbar.fire( '_selectionChangeDebounced' );
+			balloonToolbar.fire( '_selectionChangeDebounced' );
 
-			const stub = sandbox.stub( balloon, 'visibleView' ).get( () => contextualToolbar.toolbarView );
+			const stub = sandbox.stub( balloon, 'visibleView' ).get( () => balloonToolbar.toolbarView );
 
 			sinon.assert.calledOnce( showPanelSpy );
 			sinon.assert.notCalled( hidePanelSpy );
@@ -461,7 +460,7 @@ describe( 'ContextualToolbar', () => {
 		it( 'should not hide if the editor loses focus and #toolbarView is not visible', () => {
 			editor.ui.focusTracker.isFocused = true;
 
-			contextualToolbar.fire( '_selectionChangeDebounced' );
+			balloonToolbar.fire( '_selectionChangeDebounced' );
 
 			const stub = sandbox.stub( balloon, 'visibleView' ).get( () => null );
 
@@ -481,10 +480,10 @@ describe( 'ContextualToolbar', () => {
 		it( 'should fire `show` event just before panel shows', () => {
 			const spy = sandbox.spy();
 
-			contextualToolbar.on( 'show', spy );
+			balloonToolbar.on( 'show', spy );
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 			sinon.assert.calledOnce( spy );
 		} );
 
@@ -493,9 +492,9 @@ describe( 'ContextualToolbar', () => {
 
 			setData( model, '<paragraph>b[a]r</paragraph>' );
 
-			contextualToolbar.on( 'show', evt => evt.stop(), { priority: 'high' } );
+			balloonToolbar.on( 'show', evt => evt.stop(), { priority: 'high' } );
 
-			contextualToolbar.show();
+			balloonToolbar.show();
 			sinon.assert.notCalled( balloonAddSpy );
 		} );
 	} );

+ 10 - 0
packages/ckeditor5-ui/theme/components/dropdown/listdropdown.css

@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/*
+ * Note: This file should contain the wireframe styles only. But since there are no such styles,
+ * it acts as a message to the builder telling that it should look for the corresponding styles
+ * **in the theme** when compiling the editor.
+ */

+ 7 - 0
packages/ckeditor5-ui/theme/components/dropdown/splitbutton.css

@@ -3,6 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
+@import "../tooltip/mixins/_tooltip.css";
+
 .ck-splitbutton {
 	/* Enable font size inheritance, which allows fluid UI scaling. */
 	font-size: inherit;
@@ -11,3 +13,8 @@
 		z-index: calc(var(--ck-z-default) + 1);
 	}
 }
+
+/* Disable tooltips for the buttons when the button is "open" */
+.ck-splitbutton.ck-splitbutton_open > .ck-button {
+	@mixin ck-tooltip_disabled;
+}