Sfoglia il codice sorgente

Align feature class naming to a new scheme.

Maciej Gołaszewski 7 anni fa
parent
commit
f0e278ee5f

+ 10 - 115
packages/ckeditor5-heading/src/heading.js

@@ -7,20 +7,17 @@
  * @module heading/heading
  */
 
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import HeadingEngine from './headingengine';
+import HeadingEditing from './headingediting';
+import HeadingUI from './headingui';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import Model from '@ckeditor/ckeditor5-ui/src/model';
-
-import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
-
-import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
 import '../theme/heading.css';
 
 /**
- * The headings feature. It introduces the `headings` drop-down and the `heading1`-`headingN` commands which allow
- * to convert paragraphs into headings.
+ * The headings feature.
+ *
+ * It loads the {@link module:heading/headingediting~HeadingEditing heading editing feature}
+ * and {@link module:heading/headingui~HeadingUI heading UI feature}.
  *
  * For a detailed overview, check the {@glink features/headings Headings feature documentation}.
  *
@@ -31,7 +28,7 @@ export default class Heading extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ Paragraph, HeadingEngine ];
+		return [ HeadingEditing, HeadingUI ];
 	}
 
 	/**
@@ -40,112 +37,10 @@ export default class Heading extends Plugin {
 	static get pluginName() {
 		return 'Heading';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const t = editor.t;
-		const options = this._getLocalizedOptions();
-		const defaultTitle = t( 'Choose heading' );
-		const dropdownTooltip = t( 'Heading' );
-
-		// Register UI component.
-		editor.ui.componentFactory.add( 'headings', locale => {
-			const commands = [];
-			const dropdownItems = new Collection();
-
-			for ( const option of options ) {
-				const command = editor.commands.get( option.model );
-				const itemModel = new Model( {
-					commandName: option.model,
-					label: option.title,
-					class: option.class
-				} );
-
-				itemModel.bind( 'isActive' ).to( command, 'value' );
-
-				// Add the option to the collection.
-				dropdownItems.add( itemModel );
-
-				commands.push( command );
-			}
-
-			const dropdownView = createDropdown( locale );
-			addListToDropdown( dropdownView, dropdownItems );
-
-			dropdownView.buttonView.set( {
-				isOn: false,
-				withText: true,
-				tooltip: dropdownTooltip
-			} );
-
-			dropdownView.extendTemplate( {
-				attributes: {
-					class: [
-						'ck-heading-dropdown'
-					]
-				}
-			} );
-
-			dropdownView.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => {
-				return areEnabled.some( isEnabled => isEnabled );
-			} );
-
-			dropdownView.buttonView.bind( 'label' ).toMany( commands, 'value', ( ...areActive ) => {
-				const index = areActive.findIndex( value => value );
-
-				// If none of the commands is active, display default title.
-				return options[ index ] ? options[ index ].title : defaultTitle;
-			} );
-
-			// Execute command when an item from the dropdown is selected.
-			this.listenTo( dropdownView, 'execute', evt => {
-				editor.execute( evt.source.commandName );
-				editor.editing.view.focus();
-			} );
-
-			return dropdownView;
-		} );
-	}
-
-	/**
-	 * Returns heading options as defined in `config.heading.options` but processed to consider
-	 * editor localization, i.e. to display {@link module:heading/heading~HeadingOption}
-	 * in the correct language.
-	 *
-	 * Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t}
-	 * when the user config is defined because the editor does not exist yet.
-	 *
-	 * @private
-	 * @returns {Array.<module:heading/heading~HeadingOption>}.
-	 */
-	_getLocalizedOptions() {
-		const editor = this.editor;
-		const t = editor.t;
-		const localizedTitles = {
-			Paragraph: t( 'Paragraph' ),
-			'Heading 1': t( 'Heading 1' ),
-			'Heading 2': t( 'Heading 2' ),
-			'Heading 3': t( 'Heading 3' )
-		};
-
-		return editor.config.get( 'heading.options' ).map( option => {
-			const title = localizedTitles[ option.title ];
-
-			if ( title && title != option.title ) {
-				// Clone the option to avoid altering the original `config.heading.options`.
-				option = Object.assign( {}, option, { title } );
-			}
-
-			return option;
-		} );
-	}
 }
 
 /**
- * The configuration of the heading feature. Introduced by the {@link module:heading/headingengine~HeadingEngine} feature.
+ * The configuration of the heading feature. Introduced by the {@link module:heading/headingediting~HeadingEditing} feature.
  *
  * Read more in {@link module:heading/heading~HeadingConfig}.
  *
@@ -154,7 +49,7 @@ export default class Heading extends Plugin {
 
 /**
  * The configuration of the heading feature.
- * The option is used by the {@link module:heading/headingengine~HeadingEngine} feature.
+ * The option is used by the {@link module:heading/headingediting~HeadingEditing} feature.
  *
  *		ClassicEditor
  *			.create( {
@@ -191,7 +86,7 @@ export default class Heading extends Plugin {
  * Usually, the first option in the headings dropdown is the "Paragraph" option, hence it's also defined on the list.
  * However, you don't need to define its view representation because it's handled by
  * the {@link module:paragraph/paragraph~Paragraph} feature (which is required by
- * the {@link module:heading/headingengine~HeadingEngine} feature).
+ * the {@link module:heading/headingediting~HeadingEditing} feature).
  *
  * You can **read more** about configuring heading levels and **see more examples** in
  * the {@glink features/headings Headings} guide.

+ 3 - 2
packages/ckeditor5-heading/src/headingengine.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module heading/headingengine
+ * @module heading/headingediting
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -18,10 +18,11 @@ const defaultModelElement = 'paragraph';
 /**
  * The headings engine feature. It handles switching between block formats &ndash; headings and paragraph.
  * This class represents the engine part of the heading feature. See also {@link module:heading/heading~Heading}.
+ * It introduces `heading1`-`headingN` commands which allow to convert paragraphs into headings.
  *
  * @extends module:core/plugin~Plugin
  */
-export default class HeadingEngine extends Plugin {
+export default class HeadingEditing extends Plugin {
 	/**
 	 * @inheritDoc
 	 */

+ 202 - 0
packages/ckeditor5-heading/src/headingui.js

@@ -0,0 +1,202 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module heading/headingui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Model from '@ckeditor/ckeditor5-ui/src/model';
+
+import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+
+import '../theme/heading.css';
+
+/**
+ * The headings UI feature. It introduces the `headings` drop-down.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class HeadingUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+		const options = this._getLocalizedOptions();
+		const defaultTitle = t( 'Choose heading' );
+		const dropdownTooltip = t( 'Heading' );
+
+		// Register UI component.
+		editor.ui.componentFactory.add( 'headings', locale => {
+			const commands = [];
+			const dropdownItems = new Collection();
+
+			for ( const option of options ) {
+				const command = editor.commands.get( option.model );
+				const itemModel = new Model( {
+					commandName: option.model,
+					label: option.title,
+					class: option.class
+				} );
+
+				itemModel.bind( 'isActive' ).to( command, 'value' );
+
+				// Add the option to the collection.
+				dropdownItems.add( itemModel );
+
+				commands.push( command );
+			}
+
+			const dropdownView = createDropdown( locale );
+			addListToDropdown( dropdownView, dropdownItems );
+
+			dropdownView.buttonView.set( {
+				isOn: false,
+				withText: true,
+				tooltip: dropdownTooltip
+			} );
+
+			dropdownView.extendTemplate( {
+				attributes: {
+					class: [
+						'ck-heading-dropdown'
+					]
+				}
+			} );
+
+			dropdownView.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => {
+				return areEnabled.some( isEnabled => isEnabled );
+			} );
+
+			dropdownView.buttonView.bind( 'label' ).toMany( commands, 'value', ( ...areActive ) => {
+				const index = areActive.findIndex( value => value );
+
+				// If none of the commands is active, display default title.
+				return options[ index ] ? options[ index ].title : defaultTitle;
+			} );
+
+			// Execute command when an item from the dropdown is selected.
+			this.listenTo( dropdownView, 'execute', evt => {
+				editor.execute( evt.source.commandName );
+				editor.editing.view.focus();
+			} );
+
+			return dropdownView;
+		} );
+	}
+
+	/**
+	 * Returns heading options as defined in `config.heading.options` but processed to consider
+	 * editor localization, i.e. to display {@link module:heading/heading~HeadingOption}
+	 * in the correct language.
+	 *
+	 * Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t}
+	 * when the user config is defined because the editor does not exist yet.
+	 *
+	 * @private
+	 * @returns {Array.<module:heading/heading~HeadingOption>}.
+	 */
+	_getLocalizedOptions() {
+		const editor = this.editor;
+		const t = editor.t;
+		const localizedTitles = {
+			Paragraph: t( 'Paragraph' ),
+			'Heading 1': t( 'Heading 1' ),
+			'Heading 2': t( 'Heading 2' ),
+			'Heading 3': t( 'Heading 3' )
+		};
+
+		return editor.config.get( 'heading.options' ).map( option => {
+			const title = localizedTitles[ option.title ];
+
+			if ( title && title != option.title ) {
+				// Clone the option to avoid altering the original `config.heading.options`.
+				option = Object.assign( {}, option, { title } );
+			}
+
+			return option;
+		} );
+	}
+}
+
+/**
+ * The configuration of the heading feature. Introduced by the {@link module:heading/headingediting~HeadingEditing} feature.
+ *
+ * Read more in {@link module:heading/heading~HeadingConfig}.
+ *
+ * @member {module:heading/heading~HeadingConfig} module:core/editor/editorconfig~EditorConfig#heading
+ */
+
+/**
+ * The configuration of the heading feature.
+ * The option is used by the {@link module:heading/headingediting~HeadingEditing} feature.
+ *
+ *		ClassicEditor
+ *			.create( {
+ * 				heading: ... // Heading feature config.
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
+ *
+ * @interface HeadingConfig
+ */
+
+/**
+ * The available heading options.
+ *
+ * The default value is:
+ *
+ *		const headingConfig = {
+ *			options: [
+ *				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+ *				{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
+ *				{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
+ *				{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
+ *			]
+ *		};
+ *
+ * It defines 3 levels of headings. In the editor model they will use `heading1`, `heading2`, and `heading3` elements.
+ * Their respective view elements (so the elements output by the editor) will be: `h2`, `h3`, and `h4`. This means that
+ * if you choose "Heading 1" in the headings dropdown the editor will turn the current block to `<heading1>` in the model
+ * which will result in rendering (and outputting to data) the `<h2>` element.
+ *
+ * The `title` and `class` properties will be used by the `headings` dropdown to render available options.
+ * Usually, the first option in the headings dropdown is the "Paragraph" option, hence it's also defined on the list.
+ * However, you don't need to define its view representation because it's handled by
+ * the {@link module:paragraph/paragraph~Paragraph} feature (which is required by
+ * the {@link module:heading/headingediting~HeadingEditing} feature).
+ *
+ * You can **read more** about configuring heading levels and **see more examples** in
+ * the {@glink features/headings Headings} guide.
+ *
+ * Note: In the model you should always start from `heading1`, regardless of how the headings are represented in the view.
+ * That's assumption is used by features like {@link module:autoformat/autoformat~Autoformat} to know which element
+ * they should use when applying the first level heading.
+ *
+ * The defined headings are also available in {@link module:core/commandcollection~CommandCollection} under their model names.
+ * For example, the below code will apply `<heading1>` to the current selection:
+ *
+ *		editor.execute( 'heading1' );
+ *
+ * @member {Array.<module:heading/heading~HeadingOption>} module:heading/heading~HeadingConfig#options
+ */
+
+/**
+ * Heading option descriptor.
+ *
+ * This format is compatible with {@link module:engine/conversion/definition-based-converters~ConverterDefinition}
+ * and adds to additional properties: `title` and `class`.
+ *
+ * @typedef {Object} module:heading/heading~HeadingOption
+ * @extends module:engine/conversion/definition-based-converters~ConverterDefinition
+ * @property {String} title The user-readable title of the option.
+ * @property {String} class The class which will be added to the dropdown item representing this option.
+ */

+ 6 - 264
packages/ckeditor5-heading/tests/heading.js

@@ -3,274 +3,16 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals document */
-
-import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Heading from '../src/heading';
-import HeadingEngine from '../src/headingengine';
-import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
-import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-
-testUtils.createSinonSandbox();
+import HeadingEditing from '../src/headingediting';
+import HeadingUI from '../src/headingui';
 
 describe( 'Heading', () => {
-	let editor, editorElement, dropdown;
-
-	before( () => {
-		addTranslations( 'en', {
-			'Choose heading': 'Choose heading',
-			'Paragraph': 'Paragraph',
-			'Heading': 'Heading',
-			'Heading 1': 'Heading 1',
-			'Heading 2': 'Heading 2',
-		} );
-
-		addTranslations( 'pl', {
-			'Choose heading': 'Wybierz nagłówek',
-			'Paragraph': 'Akapit',
-			'Heading': 'Nagłówek',
-			'Heading 1': 'Nagłówek 1',
-			'Heading 2': 'Nagłówek 2',
-		} );
-	} );
-
-	after( () => {
-		clearTranslations();
-	} );
-
-	beforeEach( () => {
-		editorElement = document.createElement( 'div' );
-		document.body.appendChild( editorElement );
-
-		return ClassicTestEditor
-			.create( editorElement, {
-				plugins: [ Heading ],
-				toolbar: [ 'heading' ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-				dropdown = editor.ui.componentFactory.create( 'headings' );
-
-				// Set data so the commands will be enabled.
-				setData( editor.model, '<paragraph>f{}oo</paragraph>' );
-			} );
-	} );
-
-	afterEach( () => {
-		editorElement.remove();
-
-		return editor.destroy();
-	} );
-
-	it( 'should be loaded', () => {
-		expect( editor.plugins.get( Heading ) ).to.be.instanceOf( Heading );
+	it( 'should require HeadingEditing and HeadingUI', () => {
+		expect( Heading.requires ).to.deep.equal( [ HeadingEditing, HeadingUI ] );
 	} );
 
-	it( 'should load HeadingEngine', () => {
-		expect( editor.plugins.get( HeadingEngine ) ).to.be.instanceOf( HeadingEngine );
-	} );
-
-	describe( 'init()', () => {
-		it( 'should register options feature component', () => {
-			const dropdown = editor.ui.componentFactory.create( 'headings' );
-
-			expect( dropdown ).to.be.instanceOf( DropdownView );
-			expect( dropdown.buttonView.isEnabled ).to.be.true;
-			expect( dropdown.buttonView.isOn ).to.be.false;
-			expect( dropdown.buttonView.label ).to.equal( 'Paragraph' );
-			expect( dropdown.buttonView.tooltip ).to.equal( 'Heading' );
-		} );
-
-		it( 'should execute format command on model execute event', () => {
-			const executeSpy = testUtils.sinon.spy( editor, 'execute' );
-			const dropdown = editor.ui.componentFactory.create( 'headings' );
-
-			dropdown.commandName = 'paragraph';
-			dropdown.fire( 'execute' );
-
-			sinon.assert.calledOnce( executeSpy );
-			sinon.assert.calledWithExactly( executeSpy, 'paragraph' );
-		} );
-
-		it( 'should focus view after command execution', () => {
-			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
-			const dropdown = editor.ui.componentFactory.create( 'headings' );
-
-			dropdown.commandName = 'paragraph';
-			dropdown.fire( 'execute' );
-
-			sinon.assert.calledOnce( focusSpy );
-		} );
-
-		it( 'should add custom CSS class to dropdown', () => {
-			const dropdown = editor.ui.componentFactory.create( 'headings' );
-
-			dropdown.render();
-
-			expect( dropdown.element.classList.contains( 'ck-heading-dropdown' ) ).to.be.true;
-		} );
-
-		describe( 'model to command binding', () => {
-			let commands;
-
-			beforeEach( () => {
-				commands = {};
-
-				editor.config.get( 'heading.options' ).forEach( ( { model } ) => {
-					commands[ model ] = editor.commands.get( model );
-				} );
-			} );
-
-			it( 'isEnabled', () => {
-				for ( const name in commands ) {
-					commands[ name ].isEnabled = false;
-				}
-
-				expect( dropdown.buttonView.isEnabled ).to.be.false;
-
-				commands.heading2.isEnabled = true;
-				expect( dropdown.buttonView.isEnabled ).to.be.true;
-			} );
-
-			it( 'label', () => {
-				for ( const name in commands ) {
-					commands[ name ].value = false;
-				}
-
-				expect( dropdown.buttonView.label ).to.equal( 'Choose heading' );
-
-				commands.heading2.value = true;
-				expect( dropdown.buttonView.label ).to.equal( 'Heading 2' );
-			} );
-		} );
-
-		describe( 'localization', () => {
-			let commands, editor, dropdown;
-
-			beforeEach( () => {
-				return localizedEditor( [
-					{ model: 'paragraph', title: 'Paragraph' },
-					{ model: 'heading1', view: { name: 'h2' }, title: 'Heading 1' },
-					{ model: 'heading2', view: { name: 'h3' }, title: 'Heading 2' }
-				] );
-			} );
-
-			it( 'does not alter the original config', () => {
-				expect( editor.config.get( 'heading.options' ) ).to.deep.equal( [
-					{ model: 'paragraph', title: 'Paragraph' },
-					{ model: 'heading1', view: { name: 'h2' }, title: 'Heading 1' },
-					{ model: 'heading2', view: { name: 'h3' }, title: 'Heading 2' }
-				] );
-			} );
-
-			it( 'works for the #buttonView', () => {
-				const buttonView = dropdown.buttonView;
-
-				// Setting manually paragraph.value to `false` because there might be some content in editor
-				// after initialisation (for example empty <p></p> inserted when editor is empty).
-				commands.paragraph.value = false;
-				expect( buttonView.label ).to.equal( 'Wybierz nagłówek' );
-				expect( buttonView.tooltip ).to.equal( 'Nagłówek' );
-
-				commands.paragraph.value = true;
-				expect( buttonView.label ).to.equal( 'Akapit' );
-
-				commands.paragraph.value = false;
-				commands.heading1.value = true;
-				expect( buttonView.label ).to.equal( 'Nagłówek 1' );
-			} );
-
-			it( 'works for the listView#items in the panel', () => {
-				const listView = dropdown.listView;
-
-				expect( listView.items.map( item => item.label ) ).to.deep.equal( [
-					'Akapit',
-					'Nagłówek 1',
-					'Nagłówek 2'
-				] );
-			} );
-
-			it( 'allows custom titles', () => {
-				return localizedEditor( [
-					{ model: 'paragraph', title: 'Custom paragraph title' },
-					{ model: 'heading1', view: { name: 'h1' }, title: 'Custom heading1 title' }
-				] ).then( () => {
-					const listView = dropdown.listView;
-
-					expect( listView.items.map( item => item.label ) ).to.deep.equal( [
-						'Custom paragraph title',
-						'Custom heading1 title',
-					] );
-				} );
-			} );
-
-			it( 'translates default using the the locale', () => {
-				return localizedEditor( [
-					{ model: 'paragraph', title: 'Paragraph' }
-				] ).then( () => {
-					const listView = dropdown.listView;
-
-					expect( listView.items.map( item => item.label ) ).to.deep.equal( [
-						'Akapit'
-					] );
-				} );
-			} );
-
-			function localizedEditor( options ) {
-				const editorElement = document.createElement( 'div' );
-				document.body.appendChild( editorElement );
-
-				return ClassicTestEditor
-					.create( editorElement, {
-						plugins: [ Heading ],
-						toolbar: [ 'heading' ],
-						language: 'pl',
-						heading: {
-							options
-						}
-					} )
-					.then( newEditor => {
-						editor = newEditor;
-						dropdown = editor.ui.componentFactory.create( 'headings' );
-						commands = {};
-
-						editor.config.get( 'heading.options' ).forEach( ( { model } ) => {
-							commands[ model ] = editor.commands.get( model );
-						} );
-
-						editorElement.remove();
-
-						return editor.destroy();
-					} );
-			}
-		} );
-
-		describe( 'class', () => {
-			it( 'is set for the listView#items in the panel', () => {
-				const listView = dropdown.listView;
-
-				expect( listView.items.map( item => item.class ) ).to.deep.equal( [
-					'ck-heading_paragraph',
-					'ck-heading_heading1',
-					'ck-heading_heading2',
-					'ck-heading_heading3'
-				] );
-			} );
-
-			it( 'reflects the #value of the commands', () => {
-				const listView = dropdown.listView;
-
-				setData( editor.model, '<heading2>f{}oo</heading2>' );
-
-				expect( listView.items.map( item => item.isActive ) ).to.deep.equal( [
-					false,
-					false,
-					true,
-					false
-				] );
-			} );
-		} );
+	it( 'should be named', () => {
+		expect( Heading.pluginName ).to.equal( 'Heading' );
 	} );
 } );

+ 7 - 7
packages/ckeditor5-heading/tests/headingengine.js

@@ -3,20 +3,20 @@
  * For licensing, see LICENSE.md.
  */
 
-import HeadingEngine from '../src/headingengine';
+import HeadingEditing from '../src/headingediting';
 import HeadingCommand from '../src/headingcommand';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import ParagraphCommand from '@ckeditor/ckeditor5-paragraph/src/paragraphcommand';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-describe( 'HeadingEngine', () => {
+describe( 'HeadingEditing', () => {
 	let editor, model;
 
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ HeadingEngine ]
+				plugins: [ HeadingEditing ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -25,7 +25,7 @@ describe( 'HeadingEngine', () => {
 	} );
 
 	it( 'should be loaded', () => {
-		expect( editor.plugins.get( HeadingEngine ) ).to.be.instanceOf( HeadingEngine );
+		expect( editor.plugins.get( HeadingEditing ) ).to.be.instanceOf( HeadingEditing );
 	} );
 
 	it( 'should load paragraph feature', () => {
@@ -79,7 +79,7 @@ describe( 'HeadingEngine', () => {
 		beforeEach( () => {
 			return VirtualTestEditor
 				.create( {
-					plugins: [ HeadingEngine ],
+					plugins: [ HeadingEditing ],
 					heading: {
 						options: [
 							{ model: 'paragraph', title: 'paragraph' },
@@ -120,7 +120,7 @@ describe( 'HeadingEngine', () => {
 	it( 'should not blow up if there\'s no enter command in the editor', () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ HeadingEngine ]
+				plugins: [ HeadingEditing ]
 			} );
 	} );
 
@@ -145,7 +145,7 @@ describe( 'HeadingEngine', () => {
 
 				return VirtualTestEditor
 					.create( {
-						plugins: [ HeadingEngine ],
+						plugins: [ HeadingEditing ],
 						heading: {
 							options
 						}

+ 269 - 0
packages/ckeditor5-heading/tests/headingui.js

@@ -0,0 +1,269 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Heading from '../src/heading';
+import HeadingEditing from '../src/headingediting';
+import HeadingUI from '../src/headingui';
+import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
+import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+testUtils.createSinonSandbox();
+
+describe( 'HeadingUI', () => {
+	let editor, editorElement, dropdown;
+
+	before( () => {
+		addTranslations( 'en', {
+			'Choose heading': 'Choose heading',
+			'Paragraph': 'Paragraph',
+			'Heading': 'Heading',
+			'Heading 1': 'Heading 1',
+			'Heading 2': 'Heading 2',
+		} );
+
+		addTranslations( 'pl', {
+			'Choose heading': 'Wybierz nagłówek',
+			'Paragraph': 'Akapit',
+			'Heading': 'Nagłówek',
+			'Heading 1': 'Nagłówek 1',
+			'Heading 2': 'Nagłówek 2',
+		} );
+	} );
+
+	after( () => {
+		clearTranslations();
+	} );
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ HeadingUI, HeadingEditing ],
+				toolbar: [ 'heading' ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				dropdown = editor.ui.componentFactory.create( 'headings' );
+
+				// Set data so the commands will be enabled.
+				setData( editor.model, '<paragraph>f{}oo</paragraph>' );
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'init()', () => {
+		it( 'should register options feature component', () => {
+			const dropdown = editor.ui.componentFactory.create( 'headings' );
+
+			expect( dropdown ).to.be.instanceOf( DropdownView );
+			expect( dropdown.buttonView.isEnabled ).to.be.true;
+			expect( dropdown.buttonView.isOn ).to.be.false;
+			expect( dropdown.buttonView.label ).to.equal( 'Paragraph' );
+			expect( dropdown.buttonView.tooltip ).to.equal( 'Heading' );
+		} );
+
+		it( 'should execute format command on model execute event', () => {
+			const executeSpy = testUtils.sinon.spy( editor, 'execute' );
+			const dropdown = editor.ui.componentFactory.create( 'headings' );
+
+			dropdown.commandName = 'paragraph';
+			dropdown.fire( 'execute' );
+
+			sinon.assert.calledOnce( executeSpy );
+			sinon.assert.calledWithExactly( executeSpy, 'paragraph' );
+		} );
+
+		it( 'should focus view after command execution', () => {
+			const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
+			const dropdown = editor.ui.componentFactory.create( 'headings' );
+
+			dropdown.commandName = 'paragraph';
+			dropdown.fire( 'execute' );
+
+			sinon.assert.calledOnce( focusSpy );
+		} );
+
+		it( 'should add custom CSS class to dropdown', () => {
+			const dropdown = editor.ui.componentFactory.create( 'headings' );
+
+			dropdown.render();
+
+			expect( dropdown.element.classList.contains( 'ck-heading-dropdown' ) ).to.be.true;
+		} );
+
+		describe( 'model to command binding', () => {
+			let commands;
+
+			beforeEach( () => {
+				commands = {};
+
+				editor.config.get( 'heading.options' ).forEach( ( { model } ) => {
+					commands[ model ] = editor.commands.get( model );
+				} );
+			} );
+
+			it( 'isEnabled', () => {
+				for ( const name in commands ) {
+					commands[ name ].isEnabled = false;
+				}
+
+				expect( dropdown.buttonView.isEnabled ).to.be.false;
+
+				commands.heading2.isEnabled = true;
+				expect( dropdown.buttonView.isEnabled ).to.be.true;
+			} );
+
+			it( 'label', () => {
+				for ( const name in commands ) {
+					commands[ name ].value = false;
+				}
+
+				expect( dropdown.buttonView.label ).to.equal( 'Choose heading' );
+
+				commands.heading2.value = true;
+				expect( dropdown.buttonView.label ).to.equal( 'Heading 2' );
+			} );
+		} );
+
+		describe( 'localization', () => {
+			let commands, editor, dropdown;
+
+			beforeEach( () => {
+				return localizedEditor( [
+					{ model: 'paragraph', title: 'Paragraph' },
+					{ model: 'heading1', view: { name: 'h2' }, title: 'Heading 1' },
+					{ model: 'heading2', view: { name: 'h3' }, title: 'Heading 2' }
+				] );
+			} );
+
+			it( 'does not alter the original config', () => {
+				expect( editor.config.get( 'heading.options' ) ).to.deep.equal( [
+					{ model: 'paragraph', title: 'Paragraph' },
+					{ model: 'heading1', view: { name: 'h2' }, title: 'Heading 1' },
+					{ model: 'heading2', view: { name: 'h3' }, title: 'Heading 2' }
+				] );
+			} );
+
+			it( 'works for the #buttonView', () => {
+				const buttonView = dropdown.buttonView;
+
+				// Setting manually paragraph.value to `false` because there might be some content in editor
+				// after initialisation (for example empty <p></p> inserted when editor is empty).
+				commands.paragraph.value = false;
+				expect( buttonView.label ).to.equal( 'Wybierz nagłówek' );
+				expect( buttonView.tooltip ).to.equal( 'Nagłówek' );
+
+				commands.paragraph.value = true;
+				expect( buttonView.label ).to.equal( 'Akapit' );
+
+				commands.paragraph.value = false;
+				commands.heading1.value = true;
+				expect( buttonView.label ).to.equal( 'Nagłówek 1' );
+			} );
+
+			it( 'works for the listView#items in the panel', () => {
+				const listView = dropdown.listView;
+
+				expect( listView.items.map( item => item.label ) ).to.deep.equal( [
+					'Akapit',
+					'Nagłówek 1',
+					'Nagłówek 2'
+				] );
+			} );
+
+			it( 'allows custom titles', () => {
+				return localizedEditor( [
+					{ model: 'paragraph', title: 'Custom paragraph title' },
+					{ model: 'heading1', view: { name: 'h1' }, title: 'Custom heading1 title' }
+				] ).then( () => {
+					const listView = dropdown.listView;
+
+					expect( listView.items.map( item => item.label ) ).to.deep.equal( [
+						'Custom paragraph title',
+						'Custom heading1 title',
+					] );
+				} );
+			} );
+
+			it( 'translates default using the the locale', () => {
+				return localizedEditor( [
+					{ model: 'paragraph', title: 'Paragraph' }
+				] ).then( () => {
+					const listView = dropdown.listView;
+
+					expect( listView.items.map( item => item.label ) ).to.deep.equal( [
+						'Akapit'
+					] );
+				} );
+			} );
+
+			function localizedEditor( options ) {
+				const editorElement = document.createElement( 'div' );
+				document.body.appendChild( editorElement );
+
+				return ClassicTestEditor
+					.create( editorElement, {
+						plugins: [ Heading ],
+						toolbar: [ 'heading' ],
+						language: 'pl',
+						heading: {
+							options
+						}
+					} )
+					.then( newEditor => {
+						editor = newEditor;
+						dropdown = editor.ui.componentFactory.create( 'headings' );
+						commands = {};
+
+						editor.config.get( 'heading.options' ).forEach( ( { model } ) => {
+							commands[ model ] = editor.commands.get( model );
+						} );
+
+						editorElement.remove();
+
+						return editor.destroy();
+					} );
+			}
+		} );
+
+		describe( 'class', () => {
+			it( 'is set for the listView#items in the panel', () => {
+				const listView = dropdown.listView;
+
+				expect( listView.items.map( item => item.class ) ).to.deep.equal( [
+					'ck-heading_paragraph',
+					'ck-heading_heading1',
+					'ck-heading_heading2',
+					'ck-heading_heading3'
+				] );
+			} );
+
+			it( 'reflects the #value of the commands', () => {
+				const listView = dropdown.listView;
+
+				setData( editor.model, '<heading2>f{}oo</heading2>' );
+
+				expect( listView.items.map( item => item.isActive ) ).to.deep.equal( [
+					false,
+					false,
+					true,
+					false
+				] );
+			} );
+		} );
+	} );
+} );

+ 3 - 3
packages/ckeditor5-heading/tests/tickets/40.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import HeadingEngine from '../../src/headingengine';
+import HeadingEditing from '../../src/headingediting';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -18,7 +18,7 @@ describe( 'Bug ckeditor5-heading#40', () => {
 	it( 'enter at the end of a heading creates a paragraph, when heading was loaded before enter', () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ HeadingEngine, Enter ]
+				plugins: [ HeadingEditing, Enter ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -34,7 +34,7 @@ describe( 'Bug ckeditor5-heading#40', () => {
 	it( 'enter at the end of a heading creates a paragraph, when enter was loaded before heading', () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ Enter, HeadingEngine ]
+				plugins: [ Enter, HeadingEditing ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;