Explorar el Código

Merge branch 'master' into t/ckeditor5-engine/738

Kamil Piechaczek hace 8 años
padre
commit
c5a7db51b6
Se han modificado 33 ficheros con 1142 adiciones y 1044 borrados
  1. 1 1
      packages/ckeditor5-image/docs/features/image.md
  2. 3 23
      packages/ckeditor5-image/src/image.js
  3. 4 9
      packages/ckeditor5-image/src/image/imageediting.js
  4. 2 2
      packages/ckeditor5-image/src/imagecaption.js
  5. 2 2
      packages/ckeditor5-image/src/imagecaption/imagecaptionediting.js
  6. 12 53
      packages/ckeditor5-image/src/imagestyle.js
  7. 4 4
      packages/ckeditor5-image/src/imagestyle/converters.js
  8. 2 2
      packages/ckeditor5-image/src/imagestyle/imagestylecommand.js
  9. 95 0
      packages/ckeditor5-image/src/imagestyle/imagestyleediting.js
  10. 0 293
      packages/ckeditor5-image/src/imagestyle/imagestyleengine.js
  11. 109 0
      packages/ckeditor5-image/src/imagestyle/imagestyleui.js
  12. 153 0
      packages/ckeditor5-image/src/imagestyle/utils.js
  13. 5 171
      packages/ckeditor5-image/src/imagetextalternative.js
  14. 2 2
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeediting.js
  15. 194 0
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js
  16. 19 0
      packages/ckeditor5-image/src/imagetoolbar.js
  17. 3 3
      packages/ckeditor5-image/tests/image.js
  18. 1 1
      packages/ckeditor5-image/tests/image/converters.js
  19. 4 4
      packages/ckeditor5-image/tests/image/imageediting.js
  20. 3 3
      packages/ckeditor5-image/tests/imagecaption.js
  21. 6 6
      packages/ckeditor5-image/tests/imagecaption/imagecaptionediting.js
  22. 7 52
      packages/ckeditor5-image/tests/imagestyle.js
  23. 15 203
      packages/ckeditor5-image/tests/imagestyle/imagestyleediting.js
  24. 134 0
      packages/ckeditor5-image/tests/imagestyle/imagestyleui.js
  25. 111 0
      packages/ckeditor5-image/tests/imagestyle/utils.js
  26. 7 196
      packages/ckeditor5-image/tests/imagetextalternative.js
  27. 5 4
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeediting.js
  28. 228 0
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js
  29. 1 1
      packages/ckeditor5-image/tests/imageupload/imageuploadcommand.js
  30. 3 3
      packages/ckeditor5-image/tests/imageupload/imageuploadediting.js
  31. 2 2
      packages/ckeditor5-image/tests/imageupload/imageuploadprogress.js
  32. 3 2
      packages/ckeditor5-image/tests/integration.js
  33. 2 2
      packages/ckeditor5-image/tests/manual/imageplaceholder.js

+ 1 - 1
packages/ckeditor5-image/docs/features/image.md

@@ -148,7 +148,7 @@ See the result below:
 
 
 ### Defining custom styles
 ### Defining custom styles
 
 
-Besides using the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles 5 predefined styles}:
+Besides using the {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing.defaultStyles 5 predefined styles}:
 
 
 * `'imageStyleFull'`,
 * `'imageStyleFull'`,
 * `'imageStyleSide'`,
 * `'imageStyleSide'`,

+ 3 - 23
packages/ckeditor5-image/src/image.js

@@ -8,17 +8,16 @@
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageEngine from './image/imageengine';
+import ImageEditing from '../src/image/imageediting';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import ImageTextAlternative from './imagetextalternative';
 import ImageTextAlternative from './imagetextalternative';
-import { isImageWidgetSelected } from './image/utils';
 
 
 import '../theme/image.css';
 import '../theme/image.css';
 
 
 /**
 /**
  * The image plugin.
  * The image plugin.
  *
  *
- * Uses the {@link module:image/image/imageengine~ImageEngine}.
+ * Uses the {@link module:image/image/imageediting~ImageEditing}.
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
@@ -27,7 +26,7 @@ export default class Image extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	static get requires() {
 	static get requires() {
-		return [ ImageEngine, Widget, ImageTextAlternative ];
+		return [ ImageEditing, Widget, ImageTextAlternative ];
 	}
 	}
 
 
 	/**
 	/**
@@ -36,25 +35,6 @@ export default class Image extends Plugin {
 	static get pluginName() {
 	static get pluginName() {
 		return 'Image';
 		return 'Image';
 	}
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
-
-		// If `BalloonToolbar` plugin is loaded, it should be disabled for images
-		// which have their own toolbar to avoid duplication.
-		// https://github.com/ckeditor/ckeditor5-image/issues/110
-		if ( balloonToolbar ) {
-			this.listenTo( balloonToolbar, 'show', evt => {
-				if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) {
-					evt.stop();
-				}
-			}, { priority: 'high' } );
-		}
-	}
 }
 }
 
 
 /**
 /**

+ 4 - 9
packages/ckeditor5-image/src/image/imageengine.js → packages/ckeditor5-image/src/image/imageediting.js

@@ -4,7 +4,7 @@
  */
  */
 
 
 /**
 /**
- * @module image/image/imageengine
+ * @module image/image/imageediting
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -29,7 +29,7 @@ import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
-export default class ImageEngine extends Plugin {
+export default class ImageEditing extends Plugin {
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
@@ -47,19 +47,14 @@ export default class ImageEngine extends Plugin {
 			allowAttributes: [ 'alt', 'src', 'srcset' ]
 			allowAttributes: [ 'alt', 'src', 'srcset' ]
 		} );
 		} );
 
 
-		const dataElementCreator = ( modelElement, viewWriter ) => createImageViewElement( viewWriter );
 		conversion.for( 'dataDowncast' ).add( downcastElementToElement( {
 		conversion.for( 'dataDowncast' ).add( downcastElementToElement( {
 			model: 'image',
 			model: 'image',
-			view: dataElementCreator
+			view: ( modelElement, viewWriter ) => createImageViewElement( viewWriter )
 		} ) );
 		} ) );
 
 
-		const editingElementCreator = ( modelElement, viewWriter ) => {
-			return toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) );
-		};
-
 		conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
 		conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
 			model: 'image',
 			model: 'image',
-			view: editingElementCreator
+			view: ( modelElement, viewWriter ) => toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) )
 		} ) );
 		} ) );
 
 
 		conversion.for( 'downcast' )
 		conversion.for( 'downcast' )

+ 2 - 2
packages/ckeditor5-image/src/imagecaption.js

@@ -8,7 +8,7 @@
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageCaptionEngine from './imagecaption/imagecaptionengine';
+import ImageCaptionEditing from './imagecaption/imagecaptionediting';
 
 
 import '../theme/imagecaption.css';
 import '../theme/imagecaption.css';
 
 
@@ -22,7 +22,7 @@ export default class ImageCaption extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	static get requires() {
 	static get requires() {
-		return [ ImageCaptionEngine ];
+		return [ ImageCaptionEditing ];
 	}
 	}
 
 
 	/**
 	/**

+ 2 - 2
packages/ckeditor5-image/src/imagecaption/imagecaptionengine.js → packages/ckeditor5-image/src/imagecaption/imagecaptionediting.js

@@ -4,7 +4,7 @@
  */
  */
 
 
 /**
 /**
- * @module image/imagecaption/imagecaptionengine
+ * @module image/imagecaption/imagecaptionediting
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -25,7 +25,7 @@ import {
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
-export default class ImageCaptionEngine extends Plugin {
+export default class ImageCaptionEditing extends Plugin {
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */

+ 12 - 53
packages/ckeditor5-image/src/imagestyle.js

@@ -8,15 +8,14 @@
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageStyleEngine from './imagestyle/imagestyleengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-
-import '../theme/imagestyle.css';
+import ImageStyleEditing from './imagestyle/imagestyleediting';
+import ImageStyleUI from './imagestyle/imagestyleui';
 
 
 /**
 /**
  * The image style plugin.
  * The image style plugin.
  *
  *
- * Uses the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine}.
+ * It loads the {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing}
+ * and {@link module:image/imagestyle/imagestyleui~ImageStyleUI} plugins.
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
@@ -25,7 +24,7 @@ export default class ImageStyle extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	static get requires() {
 	static get requires() {
-		return [ ImageStyleEngine ];
+		return [ ImageStyleEditing, ImageStyleUI ];
 	}
 	}
 
 
 	/**
 	/**
@@ -34,55 +33,15 @@ export default class ImageStyle extends Plugin {
 	static get pluginName() {
 	static get pluginName() {
 		return 'ImageStyle';
 		return 'ImageStyle';
 	}
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const styles = editor.plugins.get( ImageStyleEngine ).imageStyles;
-
-		for ( const style of styles ) {
-			this._createButton( style );
-		}
-	}
-
-	/**
-	 * Creates a button for each style and stores it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
-	 *
-	 * @private
-	 * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
-	 */
-	_createButton( style ) {
-		const editor = this.editor;
-		const command = editor.commands.get( style.name );
-
-		editor.ui.componentFactory.add( style.name, locale => {
-			const view = new ButtonView( locale );
-
-			view.set( {
-				label: style.title,
-				icon: style.icon,
-				tooltip: true
-			} );
-
-			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
-			view.bind( 'isOn' ).to( command, 'value' );
-
-			this.listenTo( view, 'execute', () => editor.execute( style.name ) );
-
-			return view;
-		} );
-	}
 }
 }
 
 
 /**
 /**
  * Available image styles.
  * Available image styles.
- * The option is used by the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine} feature.
+ * The option is used by the {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing} feature.
  *
  *
  * The default value is:
  * The default value is:
  *
  *
- * 		const imageConfig = {
+ *		const imageConfig = {
  *			styles: [ 'imageStyleFull', 'imageStyleSide' ]
  *			styles: [ 'imageStyleFull', 'imageStyleSide' ]
  *		};
  *		};
  *
  *
@@ -91,12 +50,12 @@ export default class ImageStyle extends Plugin {
  *  * the "full" style which doesn't apply any class, e.g. for images styled to span 100% width of the content,
  *  * the "full" style which doesn't apply any class, e.g. for images styled to span 100% width of the content,
  *  * the "side" style with the `.image-style-side` CSS class.
  *  * the "side" style with the `.image-style-side` CSS class.
  *
  *
- * See {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles} to learn more about default
+ * See {@link module:image/imagestyle/utils~defaultStyles} to learn more about default
  * styles provided by the image feature.
  * styles provided by the image feature.
  *
  *
- * The {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles default styles} can be customized,
+ * The {@link module:image/imagestyle/utils~defaultStyles default styles} can be customized,
  * e.g. to change the icon, title or CSS class of the style. The feature also provides several
  * e.g. to change the icon, title or CSS class of the style. The feature also provides several
- * {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultIcons default icons} to chose from.
+ * {@link module:image/imagestyle/utils~defaultIcons default icons} to chose from.
  *
  *
  *		import customIcon from 'custom-icon.svg';
  *		import customIcon from 'custom-icon.svg';
  *
  *
@@ -129,7 +88,7 @@ export default class ImageStyle extends Plugin {
  *			]
  *			]
  *		};
  *		};
  *
  *
- * Note: Setting `title` to one of {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine#localizedDefaultStylesTitles}
+ * Note: Setting `title` to one of {@link module:image/imagestyle/imagestyleui~ImageStyleUI#localizedDefaultStylesTitles}
  * will automatically translate it to the language of the editor.
  * will automatically translate it to the language of the editor.
  *
  *
  * Read more about styling images in the {@glink features/image#Image-styles Image styles guide}.
  * Read more about styling images in the {@glink features/image#Image-styles Image styles guide}.
@@ -147,5 +106,5 @@ export default class ImageStyle extends Plugin {
  *			toolbar: [ 'imageStyleFull', 'imageStyleSide' ]
  *			toolbar: [ 'imageStyleFull', 'imageStyleSide' ]
  *		};
  *		};
  *
  *
- * @member {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>} module:image/image~ImageConfig#styles
+ * @member {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} module:image/image~ImageConfig#styles
  */
  */

+ 4 - 4
packages/ckeditor5-image/src/imagestyle/converters.js

@@ -12,7 +12,7 @@ import first from '@ckeditor/ckeditor5-utils/src/first';
 /**
 /**
  * Returns a converter for the `imageStyle` attribute. It can be used for adding, changing and removing the attribute.
  * Returns a converter for the `imageStyle` attribute. It can be used for adding, changing and removing the attribute.
  *
  *
- * @param {Object} styles An object containing available styles. See {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}
+ * @param {Object} styles An object containing available styles. See {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat}
  * for more details.
  * for more details.
  * @returns {Function} A model-to-view attribute converter.
  * @returns {Function} A model-to-view attribute converter.
  */
  */
@@ -42,7 +42,7 @@ export function modelToViewStyleAttribute( styles ) {
 /**
 /**
  * Returns a view-to-model converter converting image CSS classes to a proper value in the model.
  * Returns a view-to-model converter converting image CSS classes to a proper value in the model.
  *
  *
- * @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>} styles Styles for which the converter is created.
+ * @param {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} styles Styles for which the converter is created.
  * @returns {Function} A view-to-model converter.
  * @returns {Function} A view-to-model converter.
  */
  */
 export function viewToModelStyleAttribute( styles ) {
 export function viewToModelStyleAttribute( styles ) {
@@ -76,8 +76,8 @@ export function viewToModelStyleAttribute( styles ) {
 // Returns style with given `name` from array of styles.
 // Returns style with given `name` from array of styles.
 //
 //
 // @param {String} name
 // @param {String} name
-// @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat> } styles
-// @return {module:image/imagestyle/imagestyleengine~ImageStyleFormat|undefined}
+// @param {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat> } styles
+// @return {module:image/imagestyle/imagestyleediting~ImageStyleFormat|undefined}
 function getStyleByName( name, styles ) {
 function getStyleByName( name, styles ) {
 	for ( const style of styles ) {
 	for ( const style of styles ) {
 		if ( style.name === name ) {
 		if ( style.name === name ) {

+ 2 - 2
packages/ckeditor5-image/src/imagestyle/imagestylecommand.js

@@ -20,7 +20,7 @@ export default class ImageStyleCommand extends Command {
 	 * Creates an instance of the image style command. Each command instance is handling one style.
 	 * Creates an instance of the image style command. Each command instance is handling one style.
 	 *
 	 *
 	 * @param {module:core/editor/editor~Editor} editor The editor instance.
 	 * @param {module:core/editor/editor~Editor} editor The editor instance.
-	 * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style A style to be applied by this command.
+	 * @param {module:image/imagestyle/imagestyleediting~ImageStyleFormat} style A style to be applied by this command.
 	 */
 	 */
 	constructor( editor, style ) {
 	constructor( editor, style ) {
 		super( editor );
 		super( editor );
@@ -38,7 +38,7 @@ export default class ImageStyleCommand extends Command {
 		 * A style handled by this command.
 		 * A style handled by this command.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:image/imagestyle/imagestyleengine~ImageStyleFormat} #style
+		 * @member {module:image/imagestyle/imagestyleediting~ImageStyleFormat} #style
 		 */
 		 */
 		this.style = style;
 		this.style = style;
 	}
 	}

+ 95 - 0
packages/ckeditor5-image/src/imagestyle/imagestyleediting.js

@@ -0,0 +1,95 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagestyle/imagestyleediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ImageStyleCommand from './imagestylecommand';
+import ImageEditing from '../image/imageediting';
+import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters';
+import { normalizeImageStyles } from './utils';
+
+/**
+ * The image style engine plugin. It sets the default configuration, creates converters and registers
+ * {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}.
+ *
+ * @extends {module:core/plugin~Plugin}
+ */
+export default class ImageStyleEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ImageEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ImageStyleEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+		const data = editor.data;
+		const editing = editor.editing;
+
+		// Define default configuration.
+		editor.config.define( 'image.styles', [ 'imageStyleFull', 'imageStyleSide' ] );
+
+		// Get configuration.
+		const styles = normalizeImageStyles( editor.config.get( 'image.styles' ) );
+
+		// Allow imageStyle attribute in image.
+		// We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559.
+		schema.extend( 'image', { allowAttributes: 'imageStyle' } );
+
+		// Converters for imageStyle attribute from model to view.
+		const modelToViewConverter = modelToViewStyleAttribute( styles );
+		editing.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter );
+		data.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter );
+
+		// Converter for figure element from view to model.
+		data.upcastDispatcher.on( 'element:figure', viewToModelStyleAttribute( styles ), { priority: 'low' } );
+
+		// Register separate command for each style.
+		for ( const style of styles ) {
+			editor.commands.add( style.name, new ImageStyleCommand( editor, style ) );
+		}
+	}
+}
+
+/**
+ * Image style format descriptor.
+ *
+ *		import fullWidthIcon from 'path/to/icon.svg`;
+ *
+ *		const imageStyleFormat = {
+ *			name: 'fullSizeImage',
+ *			icon: fullWidthIcon,
+ *			title: 'Full size image',
+ *			className: 'image-full-size'
+ *		}
+ *
+ * @typedef {Object} module:image/imagestyle/imagestyleediting~ImageStyleFormat
+ * @property {String} name The unique name of the style. It will be used to:
+ * * register the {@link module:core/command~Command command} which will apply this style,
+ * * store the style's button in the editor {@link module:ui/componentfactory~ComponentFactory},
+ * * store the style in the `imageStyle` model attribute.
+ * @property {Boolean} [isDefault] When set, the style will be used as the default one.
+ * A default style does not apply any CSS class to the view element.
+ * @property {String} icon One of the following to be used when creating the style's button:
+ *  * An SVG icon source (as an XML string),
+ *  * One of {@link module:image/imagestyle/utils~defaultIcons} to use a default icon provided by the plugin.
+ * @property {String} title The style's title.
+ * @property {String} className The CSS class used to represent the style in view.
+ */

+ 0 - 293
packages/ckeditor5-image/src/imagestyle/imagestyleengine.js

@@ -1,293 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module image/imagestyle/imagestyleengine
- */
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageStyleCommand from './imagestylecommand';
-import ImageEngine from '../image/imageengine';
-import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
-import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
-import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
-import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
-
-/**
- * The image style engine plugin. It sets the default configuration, creates converters and registers
- * {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}.
- *
- * @extends {module:core/plugin~Plugin}
- */
-export default class ImageStyleEngine extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	static get requires() {
-		return [ ImageEngine ];
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'ImageStyleEngine';
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const schema = editor.model.schema;
-		const data = editor.data;
-		const editing = editor.editing;
-
-		// Define default configuration.
-		editor.config.define( 'image.styles', [ 'imageStyleFull', 'imageStyleSide' ] );
-
-		// Get configuration.
-		const styles = this.imageStyles;
-
-		// Allow imageStyle attribute in image.
-		// We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559.
-		schema.extend( 'image', { allowAttributes: 'imageStyle' } );
-
-		// Converters for imageStyle attribute from model to view.
-		const modelToViewConverter = modelToViewStyleAttribute( styles );
-		editing.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter );
-		data.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter );
-
-		// Converter for figure element from view to model.
-		data.upcastDispatcher.on( 'element:figure', viewToModelStyleAttribute( styles ), { priority: 'low' } );
-
-		// Register separate command for each style.
-		for ( const style of styles ) {
-			editor.commands.add( style.name, new ImageStyleCommand( editor, style ) );
-		}
-	}
-
-	/**
-	 * Returns {@link module:image/image~ImageConfig#styles} array with items normalized in the
-	 * {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat} format, translated
-	 * `title` and a complete `icon` markup for each style.
-	 *
-	 * @readonly
-	 * @type {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>}
-	 */
-	get imageStyles() {
-		// Return cached value if there is one to improve the performance.
-		if ( this._cachedImageStyles ) {
-			return this._cachedImageStyles;
-		}
-
-		const styles = [];
-		const editor = this.editor;
-		const titles = this.localizedDefaultStylesTitles;
-		const configuredStyles = editor.config.get( 'image.styles' );
-
-		for ( let style of configuredStyles ) {
-			style = normalizeStyle( style );
-
-			// Localize the titles of the styles, if a title corresponds with
-			// a localized default provided by the plugin.
-			if ( titles[ style.title ] ) {
-				style.title = titles[ style.title ];
-			}
-
-			// Don't override the user-defined styles array, clone it instead.
-			styles.push( style );
-		}
-
-		return ( this._cachedImageStyles = styles );
-	}
-
-	/**
-	 * Returns the default localized style titles provided by the plugin e.g. ready to
-	 * use in the {@link #imageStyles}.
-	 *
-	 * The following localized titles corresponding with
-	 * {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles} are available:
-	 *
-	 * * `'Full size image'`,
-	 * * `'Side image'`,
-	 * * `'Left aligned image'`,
-	 * * `'Centered image'`,
-	 * * `'Right aligned image'`
-	 *
-	 * @readonly
-	 * @type {Object.<String,String>}
-	 */
-	get localizedDefaultStylesTitles() {
-		const t = this.editor.t;
-
-		return {
-			'Full size image': t( 'Full size image' ),
-			'Side image': t( 'Side image' ),
-			'Left aligned image': t( 'Left aligned image' ),
-			'Centered image': t( 'Centered image' ),
-			'Right aligned image': t( 'Right aligned image' ),
-		};
-	}
-}
-
-/**
- * Default image styles provided by the plugin, which can be referred in the
- * {@link module:image/image~ImageConfig#styles} config.
- *
- * Among them, 2 default semantic content styles are available:
- *
- * * `imageStyleFull` is a full–width image without any CSS class,
- * * `imageStyleSide` is a side image styled with the `image-style-side` CSS class
- *
- * There are also 3 styles focused on formatting:
- *
- * * `imageStyleAlignLeft` aligns the image to the left using the `image-style-align-left` class,
- * * `imageStyleAlignCenter` centers the image to the left using the `image-style-align-center` class,
- * * `imageStyleAlignRight` aligns the image to the right using the `image-style-align-right` class,
- *
- * @member {Object.<String,Object>}
- */
-ImageStyleEngine.defaultStyles = {
-	// This option is equal to situation when no style is applied.
-	imageStyleFull: {
-		name: 'imageStyleFull',
-		title: 'Full size image',
-		icon: fullWidthIcon,
-		isDefault: true
-	},
-
-	// This represents side image.
-	imageStyleSide: {
-		name: 'imageStyleSide',
-		title: 'Side image',
-		icon: rightIcon,
-		className: 'image-style-side'
-	},
-
-	// This style represents an imaged aligned to the left.
-	imageStyleAlignLeft: {
-		name: 'imageStyleAlignLeft',
-		title: 'Left aligned image',
-		icon: leftIcon,
-		className: 'image-style-align-left'
-	},
-
-	// This style represents a centered imaged.
-	imageStyleAlignCenter: {
-		name: 'imageStyleAlignCenter',
-		title: 'Centered image',
-		icon: centerIcon,
-		className: 'image-style-align-center'
-	},
-
-	// This style represents an imaged aligned to the right.
-	imageStyleAlignRight: {
-		name: 'imageStyleAlignRight',
-		title: 'Right aligned image',
-		icon: rightIcon,
-		className: 'image-style-align-right'
-	}
-};
-
-/**
- * Default image style icons provided by the plugin, which can be referred in the
- * {@link module:image/image~ImageConfig#styles} config.
- *
- * There are 4 icons available: `'full'`, `'left'`, `'center'` and `'right'`.
- *
- * @member {Object.<String, String>}
- */
-ImageStyleEngine.defaultIcons = {
-	full: fullWidthIcon,
-	left: leftIcon,
-	right: rightIcon,
-	center: centerIcon,
-};
-
-// Normalizes an image style provided in the {@link module:image/image~ImageConfig#styles}
-// and returns it in a {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}.
-//
-// @private
-// @param {Object} style
-// @returns {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}
-function normalizeStyle( style ) {
-	const defaultStyles = ImageStyleEngine.defaultStyles;
-	const defaultIcons = ImageStyleEngine.defaultIcons;
-
-	// Just the name of the style has been passed.
-	if ( typeof style == 'string' ) {
-		// If it's one of the defaults, just use it.
-		// Clone the style to avoid overriding defaults.
-		if ( defaultStyles[ style ] ) {
-			style = Object.assign( {}, defaultStyles[ style ] );
-		}
-		// If it's just a name but none of the defaults, warn because probably it's a mistake.
-		else {
-			log.warn(
-				'image-style-not-found: There is no such image style of given name.',
-				{ name: style }
-			);
-
-			// Normalize the style anyway to prevent errors.
-			style = {
-				name: style
-			};
-		}
-	}
-
-	// If an object style has been passed and if the name matches one of the defaults,
-	// extend it with defaults – the user wants to customize a default style.
-	// Note: Don't override the user–defined style object, clone it instead.
-	else if ( defaultStyles[ style.name ] ) {
-		const defaultStyle = defaultStyles[ style.name ];
-		const extendedStyle = Object.assign( {}, style );
-
-		for ( const prop in defaultStyle ) {
-			if ( !style.hasOwnProperty( prop ) ) {
-				extendedStyle[ prop ] = defaultStyle[ prop ];
-			}
-		}
-
-		style = extendedStyle;
-	}
-
-	// If an icon is defined as a string and correspond with a name
-	// in default icons, use the default icon provided by the plugin.
-	if ( typeof style.icon == 'string' && defaultIcons[ style.icon ] ) {
-		style.icon = defaultIcons[ style.icon ];
-	}
-
-	return style;
-}
-
-/**
- * Image style format descriptor.
- *
- *		import fullWidthIcon from 'path/to/icon.svg`;
- *
- *		const imageStyleFormat = {
- *			name: 'fullSizeImage',
- *			icon: fullWidthIcon,
- *			title: 'Full size image',
- *			className: 'image-full-size'
- *		}
- *
- * @typedef {Object} module:image/imagestyle/imagestyleengine~ImageStyleFormat
- * @property {String} name The unique name of the style. It will be used to:
- * * register the {@link module:core/command~Command command} which will apply this style,
- * * store the style's button in the editor {@link module:ui/componentfactory~ComponentFactory},
- * * store the style in the `imageStyle` model attribute.
- * @property {Boolean} [isDefault] When set, the style will be used as the default one.
- * A default style does not apply any CSS class to the view element.
- * @property {String} icon One of the following to be used when creating the style's button:
- *  * An SVG icon source (as an XML string),
- *  * One of {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultIcons} to use a default icon provided by the plugin.
- * @property {String} title The style's title.
- * @property {String} className The CSS class used to represent the style in view.
- */

+ 109 - 0
packages/ckeditor5-image/src/imagestyle/imagestyleui.js

@@ -0,0 +1,109 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagestyle/imagestyleui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import { normalizeImageStyles } from './utils';
+
+import '../../theme/imagestyle.css';
+
+/**
+ * The image style UI plugin.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageStyleUI extends Plugin {
+	/**
+	 * Returns the default localized style titles provided by the plugin.
+	 *
+	 * The following localized titles corresponding with
+	 * {@link module:image/imagestyle/utils~defaultStyles} are available:
+	 *
+	 * * `'Full size image'`,
+	 * * `'Side image'`,
+	 * * `'Left aligned image'`,
+	 * * `'Centered image'`,
+	 * * `'Right aligned image'`
+	 *
+	 * @returns {Object.<String,String>}
+	 */
+	get localizedDefaultStylesTitles() {
+		const t = this.editor.t;
+
+		return {
+			'Full size image': t( 'Full size image' ),
+			'Side image': t( 'Side image' ),
+			'Left aligned image': t( 'Left aligned image' ),
+			'Centered image': t( 'Centered image' ),
+			'Right aligned image': t( 'Right aligned image' )
+		};
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const configuredStyles = editor.config.get( 'image.styles' );
+
+		const translatedStyles = translateStyles( normalizeImageStyles( configuredStyles ), this.localizedDefaultStylesTitles );
+
+		for ( const style of translatedStyles ) {
+			this._createButton( style );
+		}
+	}
+
+	/**
+	 * Creates a button for each style and stores it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
+	 *
+	 * @private
+	 * @param {module:image/imagestyle/imagestyleediting~ImageStyleFormat} style
+	 */
+	_createButton( style ) {
+		const editor = this.editor;
+
+		editor.ui.componentFactory.add( style.name, locale => {
+			const command = editor.commands.get( style.name );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: style.title,
+				icon: style.icon,
+				tooltip: true
+			} );
+
+			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
+			view.bind( 'isOn' ).to( command, 'value' );
+
+			this.listenTo( view, 'execute', () => editor.execute( style.name ) );
+
+			return view;
+		} );
+	}
+}
+
+/**
+ * Returns translated `title` from the passed styles array.
+ *
+ * @param {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} styles
+ * @param titles
+ * @returns {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>}
+ */
+function translateStyles( styles, titles ) {
+	for ( const style of styles ) {
+		// Localize the titles of the styles, if a title corresponds with
+		// a localized default provided by the plugin.
+		if ( titles[ style.title ] ) {
+			style.title = titles[ style.title ];
+		}
+	}
+
+	return styles;
+}

+ 153 - 0
packages/ckeditor5-image/src/imagestyle/utils.js

@@ -0,0 +1,153 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagestyle/utils
+ */
+
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
+import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
+import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
+import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
+
+/**
+ * Default image styles provided by the plugin, which can be referred in the
+ * {@link module:image/image~ImageConfig#styles} config.
+ *
+ * Among them, 2 default semantic content styles are available:
+ *
+ * * `imageStyleFull` is a full–width image without any CSS class,
+ * * `imageStyleSide` is a side image styled with the `image-style-side` CSS class
+ *
+ * There are also 3 styles focused on formatting:
+ *
+ * * `imageStyleAlignLeft` aligns the image to the left using the `image-style-align-left` class,
+ * * `imageStyleAlignCenter` centers the image to the left using the `image-style-align-center` class,
+ * * `imageStyleAlignRight` aligns the image to the right using the `image-style-align-right` class,
+ *
+ * @member {Object.<String,Object>}
+ */
+const defaultStyles = {
+	// This option is equal to situation when no style is applied.
+	imageStyleFull: {
+		name: 'imageStyleFull',
+		title: 'Full size image',
+		icon: fullWidthIcon,
+		isDefault: true
+	},
+
+	// This represents side image.
+	imageStyleSide: {
+		name: 'imageStyleSide',
+		title: 'Side image',
+		icon: rightIcon,
+		className: 'image-style-side'
+	},
+
+	// This style represents an imaged aligned to the left.
+	imageStyleAlignLeft: {
+		name: 'imageStyleAlignLeft',
+		title: 'Left aligned image',
+		icon: leftIcon,
+		className: 'image-style-align-left'
+	},
+
+	// This style represents a centered imaged.
+	imageStyleAlignCenter: {
+		name: 'imageStyleAlignCenter',
+		title: 'Centered image',
+		icon: centerIcon,
+		className: 'image-style-align-center'
+	},
+
+	// This style represents an imaged aligned to the right.
+	imageStyleAlignRight: {
+		name: 'imageStyleAlignRight',
+		title: 'Right aligned image',
+		icon: rightIcon,
+		className: 'image-style-align-right'
+	}
+};
+
+/**
+ * Default image style icons provided by the plugin, which can be referred in the
+ * {@link module:image/image~ImageConfig#styles} config.
+ *
+ * There are 4 icons available: `'full'`, `'left'`, `'center'` and `'right'`.
+ *
+ * @member {Object.<String, String>}
+ */
+const defaultIcons = {
+	full: fullWidthIcon,
+	left: leftIcon,
+	right: rightIcon,
+	center: centerIcon
+};
+
+/**
+ * Returns {@link module:image/image~ImageConfig#styles} array with items normalized in the
+ * {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat} format and a complete `icon` markup for each style.
+ *
+ * @returns {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>}
+ */
+export function normalizeImageStyles( configuredStyles = [] ) {
+	return configuredStyles
+		.map( _normalizeStyle )
+		.map( style => Object.assign( {}, style ) );
+}
+
+// Normalizes an image style provided in the {@link module:image/image~ImageConfig#styles}
+// and returns it in a {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat}.
+//
+// @param {Object} style
+// @returns {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat}
+function _normalizeStyle( style ) {
+	// Just the name of the style has been passed.
+	if ( typeof style == 'string' ) {
+		// If it's one of the defaults, just use it.
+		// Clone the style to avoid overriding defaults.
+		if ( defaultStyles[ style ] ) {
+			style = Object.assign( {}, defaultStyles[ style ] );
+		}
+		// If it's just a name but none of the defaults, warn because probably it's a mistake.
+		else {
+			log.warn(
+				'image-style-not-found: There is no such image style of given name.',
+				{ name: style }
+			);
+
+			// Normalize the style anyway to prevent errors.
+			style = {
+				name: style
+			};
+		}
+	}
+
+	// If an object style has been passed and if the name matches one of the defaults,
+	// extend it with defaults – the user wants to customize a default style.
+	// Note: Don't override the user–defined style object, clone it instead.
+	else if ( defaultStyles[ style.name ] ) {
+		const defaultStyle = defaultStyles[ style.name ];
+		const extendedStyle = Object.assign( {}, style );
+
+		for ( const prop in defaultStyle ) {
+			if ( !style.hasOwnProperty( prop ) ) {
+				extendedStyle[ prop ] = defaultStyle[ prop ];
+			}
+		}
+
+		style = extendedStyle;
+	}
+
+	// If an icon is defined as a string and correspond with a name
+	// in default icons, use the default icon provided by the plugin.
+	if ( typeof style.icon == 'string' && defaultIcons[ style.icon ] ) {
+		style.icon = defaultIcons[ style.icon ];
+	}
+
+	return style;
+}

+ 5 - 171
packages/ckeditor5-image/src/imagetextalternative.js

@@ -8,19 +8,14 @@
  */
  */
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import ImageTextAlternativeEngine from './imagetextalternative/imagetextalternativeengine';
-import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
-import TextAlternativeFormView from './imagetextalternative/ui/textalternativeformview';
-import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
-import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
-import { repositionContextualBalloon, getBalloonPositionData } from './image/ui/utils';
-import { isImageWidgetSelected } from './image/utils';
+import ImageTextAlternativeEditing from './imagetextalternative/imagetextalternativeediting';
+import ImageTextAlternativeUI from './imagetextalternative/imagetextalternativeui';
 
 
 /**
 /**
  * The image text alternative plugin.
  * The image text alternative plugin.
  *
  *
- * The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
+ * It loads the {@link module:image/imagetextalternative/imagetextalternativeediting~ImageTextAlternativeEditing}
+ * and {@link module:image/imagetextalternative/imagetextalternativeui~ImageTextAlternativeUI} plugins.
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
@@ -29,7 +24,7 @@ export default class ImageTextAlternative extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	static get requires() {
 	static get requires() {
-		return [ ImageTextAlternativeEngine, ContextualBalloon ];
+		return [ ImageTextAlternativeEditing, ImageTextAlternativeUI ];
 	}
 	}
 
 
 	/**
 	/**
@@ -38,165 +33,4 @@ export default class ImageTextAlternative extends Plugin {
 	static get pluginName() {
 	static get pluginName() {
 		return 'ImageTextAlternative';
 		return 'ImageTextAlternative';
 	}
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		this._createButton();
-		this._createForm();
-	}
-
-	/**
-	 * Creates a button showing the balloon panel for changing the image text alternative and
-	 * registers it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
-	 *
-	 * @private
-	 */
-	_createButton() {
-		const editor = this.editor;
-		const command = editor.commands.get( 'imageTextAlternative' );
-		const t = editor.t;
-
-		editor.ui.componentFactory.add( 'imageTextAlternative', locale => {
-			const view = new ButtonView( locale );
-
-			view.set( {
-				label: t( 'Change image text alternative' ),
-				icon: textAlternativeIcon,
-				tooltip: true
-			} );
-
-			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
-
-			this.listenTo( view, 'execute', () => this._showForm() );
-
-			return view;
-		} );
-	}
-
-	/**
-	 * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
-	 * form.
-	 *
-	 * @private
-	 */
-	_createForm() {
-		const editor = this.editor;
-		const view = editor.editing.view;
-		const viewDocument = view.document;
-
-		/**
-		 * The contextual balloon plugin instance.
-		 *
-		 * @private
-		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
-		 */
-		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
-
-		/**
-		 * A form containing a textarea and buttons, used to change the `alt` text value.
-		 *
-		 * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form
-		 */
-		this._form = new TextAlternativeFormView( editor.locale );
-
-		// Render the form so its #element is available for clickOutsideHandler.
-		this._form.render();
-
-		this.listenTo( this._form, 'submit', () => {
-			editor.execute( 'imageTextAlternative', {
-				newValue: this._form.labeledInput.inputView.element.value
-			} );
-
-			this._hideForm( true );
-		} );
-
-		this.listenTo( this._form, 'cancel', () => {
-			this._hideForm( true );
-		} );
-
-		// Close the form on Esc key press.
-		this._form.keystrokes.set( 'Esc', ( data, cancel ) => {
-			this._hideForm( true );
-			cancel();
-		} );
-
-		// Reposition the balloon or hide the form if an image widget is no longer selected.
-		this.listenTo( view, 'render', () => {
-			if ( !isImageWidgetSelected( viewDocument.selection ) ) {
-				this._hideForm( true );
-			} else if ( this._isVisible ) {
-				repositionContextualBalloon( editor );
-			}
-		} );
-
-		// Close on click outside of balloon panel element.
-		clickOutsideHandler( {
-			emitter: this._form,
-			activator: () => this._isVisible,
-			contextElements: [ this._form.element ],
-			callback: () => this._hideForm()
-		} );
-	}
-
-	/**
-	 * Shows the {@link #_form} in the {@link #_balloon}.
-	 *
-	 * @private
-	 */
-	_showForm() {
-		if ( this._isVisible ) {
-			return;
-		}
-
-		const editor = this.editor;
-		const command = editor.commands.get( 'imageTextAlternative' );
-		const labeledInput = this._form.labeledInput;
-
-		if ( !this._balloon.hasView( this._form ) ) {
-			this._balloon.add( {
-				view: this._form,
-				position: getBalloonPositionData( editor )
-			} );
-		}
-
-		// Make sure that each time the panel shows up, the field remains in sync with the value of
-		// the command. If the user typed in the input, then canceled the balloon (`labeledInput#value`
-		// stays unaltered) and re-opened it without changing the value of the command, they would see the
-		// old value instead of the actual value of the command.
-		// https://github.com/ckeditor/ckeditor5-image/issues/114
-		labeledInput.value = labeledInput.inputView.element.value = command.value || '';
-
-		this._form.labeledInput.select();
-	}
-
-	/**
-	 * Removes the {@link #_form} from the {@link #_balloon}.
-	 *
-	 * @param {Boolean} [focusEditable=false] Controls whether the editing view is focused afterwards.
-	 * @private
-	 */
-	_hideForm( focusEditable ) {
-		if ( !this._isVisible ) {
-			return;
-		}
-
-		this._balloon.remove( this._form );
-
-		if ( focusEditable ) {
-			this.editor.editing.view.focus();
-		}
-	}
-
-	/**
-	 * Returns `true` when the {@link #_form} is the visible view
-	 * in the {@link #_balloon}.
-	 *
-	 * @private
-	 * @type {Boolean}
-	 */
-	get _isVisible() {
-		return this._balloon.visibleView == this._form;
-	}
 }
 }

+ 2 - 2
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeengine.js → packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeediting.js

@@ -4,7 +4,7 @@
  */
  */
 
 
 /**
 /**
- * @module image/imagetextalternative/imagetextalternativeengine
+ * @module image/imagetextalternative/imagetextalternativeediting
  */
  */
 
 
 import ImageTextAlternativeCommand from './imagetextalternativecommand';
 import ImageTextAlternativeCommand from './imagetextalternativecommand';
@@ -16,7 +16,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
-export default class ImageTextAlternativeEngine extends Plugin {
+export default class ImageTextAlternativeEditing extends Plugin {
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */

+ 194 - 0
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js

@@ -0,0 +1,194 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagetextalternative/imagetextalternativeui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
+import TextAlternativeFormView from './ui/textalternativeformview';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
+import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
+import { repositionContextualBalloon, getBalloonPositionData } from '../image/ui/utils';
+import { isImageWidgetSelected } from '../image/utils';
+
+/**
+ * The image text alternative UI plugin.
+ *
+ * The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageTextAlternativeUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ContextualBalloon ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this._createButton();
+		this._createForm();
+	}
+
+	/**
+	 * Creates a button showing the balloon panel for changing the image text alternative and
+	 * registers it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
+	 *
+	 * @private
+	 */
+	_createButton() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		editor.ui.componentFactory.add( 'imageTextAlternative', locale => {
+			const command = editor.commands.get( 'imageTextAlternative' );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: t( 'Change image text alternative' ),
+				icon: textAlternativeIcon,
+				tooltip: true
+			} );
+
+			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
+
+			this.listenTo( view, 'execute', () => this._showForm() );
+
+			return view;
+		} );
+	}
+
+	/**
+	 * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
+	 * form.
+	 *
+	 * @private
+	 */
+	_createForm() {
+		const editor = this.editor;
+		const view = editor.editing.view;
+		const viewDocument = view.document;
+
+		/**
+		 * The contextual balloon plugin instance.
+		 *
+		 * @private
+		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
+		 */
+		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
+
+		/**
+		 * A form containing a textarea and buttons, used to change the `alt` text value.
+		 *
+		 * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form
+		 */
+		this._form = new TextAlternativeFormView( editor.locale );
+
+		// Render the form so its #element is available for clickOutsideHandler.
+		this._form.render();
+
+		this.listenTo( this._form, 'submit', () => {
+			editor.execute( 'imageTextAlternative', {
+				newValue: this._form.labeledInput.inputView.element.value
+			} );
+
+			this._hideForm( true );
+		} );
+
+		this.listenTo( this._form, 'cancel', () => {
+			this._hideForm( true );
+		} );
+
+		// Close the form on Esc key press.
+		this._form.keystrokes.set( 'Esc', ( data, cancel ) => {
+			this._hideForm( true );
+			cancel();
+		} );
+
+		// Reposition the balloon or hide the form if an image widget is no longer selected.
+		this.listenTo( view, 'render', () => {
+			if ( !isImageWidgetSelected( viewDocument.selection ) ) {
+				this._hideForm( true );
+			} else if ( this._isVisible ) {
+				repositionContextualBalloon( editor );
+			}
+		} );
+
+		// Close on click outside of balloon panel element.
+		clickOutsideHandler( {
+			emitter: this._form,
+			activator: () => this._isVisible,
+			contextElements: [ this._form.element ],
+			callback: () => this._hideForm()
+		} );
+	}
+
+	/**
+	 * Shows the {@link #_form} in the {@link #_balloon}.
+	 *
+	 * @private
+	 */
+	_showForm() {
+		if ( this._isVisible ) {
+			return;
+		}
+
+		const editor = this.editor;
+		const command = editor.commands.get( 'imageTextAlternative' );
+		const labeledInput = this._form.labeledInput;
+
+		if ( !this._balloon.hasView( this._form ) ) {
+			this._balloon.add( {
+				view: this._form,
+				position: getBalloonPositionData( editor )
+			} );
+		}
+
+		// Make sure that each time the panel shows up, the field remains in sync with the value of
+		// the command. If the user typed in the input, then canceled the balloon (`labeledInput#value`
+		// stays unaltered) and re-opened it without changing the value of the command, they would see the
+		// old value instead of the actual value of the command.
+		// https://github.com/ckeditor/ckeditor5-image/issues/114
+		labeledInput.value = labeledInput.inputView.element.value = command.value || '';
+
+		this._form.labeledInput.select();
+	}
+
+	/**
+	 * Removes the {@link #_form} from the {@link #_balloon}.
+	 *
+	 * @param {Boolean} [focusEditable=false] Controls whether the editing view is focused afterwards.
+	 * @private
+	 */
+	_hideForm( focusEditable ) {
+		if ( !this._isVisible ) {
+			return;
+		}
+
+		this._balloon.remove( this._form );
+
+		if ( focusEditable ) {
+			this.editor.editing.view.focus();
+		}
+	}
+
+	/**
+	 * Returns `true` when the {@link #_form} is the visible view
+	 * in the {@link #_balloon}.
+	 *
+	 * @private
+	 * @type {Boolean}
+	 */
+	get _isVisible() {
+		return this._balloon.visibleView == this._form;
+	}
+}

+ 19 - 0
packages/ckeditor5-image/src/imagetoolbar.js

@@ -40,6 +40,25 @@ export default class ImageToolbar extends Plugin {
 		return 'ImageToolbar';
 		return 'ImageToolbar';
 	}
 	}
 
 
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
+
+		// If `BalloonToolbar` plugin is loaded, it should be disabled for images
+		// which have their own toolbar to avoid duplication.
+		// https://github.com/ckeditor/ckeditor5-image/issues/110
+		if ( balloonToolbar ) {
+			this.listenTo( balloonToolbar, 'show', evt => {
+				if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) {
+					evt.stop();
+				}
+			}, { priority: 'high' } );
+		}
+	}
+
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */

+ 3 - 3
packages/ckeditor5-image/tests/image.js

@@ -5,7 +5,7 @@
 
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Image from '../src/image';
 import Image from '../src/image';
-import ImageEngine from '../src/image/imageengine';
+import ImageEditing from '../src/image/imageediting';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import ImageTextAlternative from '../src/imagetextalternative';
 import ImageTextAlternative from '../src/imagetextalternative';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -43,8 +43,8 @@ describe( 'Image', () => {
 		expect( editor.plugins.get( Image ) ).to.instanceOf( Image );
 		expect( editor.plugins.get( Image ) ).to.instanceOf( Image );
 	} );
 	} );
 
 
-	it( 'should load ImageEngine plugin', () => {
-		expect( editor.plugins.get( ImageEngine ) ).to.instanceOf( ImageEngine );
+	it( 'should load ImageEditing plugin', () => {
+		expect( editor.plugins.get( ImageEditing ) ).to.instanceOf( ImageEditing );
 	} );
 	} );
 
 
 	it( 'should load Widget plugin', () => {
 	it( 'should load Widget plugin', () => {

+ 1 - 1
packages/ckeditor5-image/tests/image/converters.js

@@ -8,7 +8,7 @@ import {
 	modelToViewAttributeConverter
 	modelToViewAttributeConverter
 } from '../../src/image/converters';
 } from '../../src/image/converters';
 import { toImageWidget } from '../../src/image/utils';
 import { toImageWidget } from '../../src/image/utils';
-import { createImageViewElement } from '../../src/image/imageengine';
+import { createImageViewElement } from '../../src/image/imageediting';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 
 
 import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';

+ 4 - 4
packages/ckeditor5-image/tests/image/imageengine.js → packages/ckeditor5-image/tests/image/imageediting.js

@@ -4,19 +4,19 @@
  */
  */
 
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageEngine from '../../src/image/imageengine';
+import ImageEditing from '../../src/image/imageediting';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { isImageWidget } from '../../src/image/utils';
 import { isImageWidget } from '../../src/image/utils';
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 
 
-describe( 'ImageEngine', () => {
+describe( 'ImageEditing', () => {
 	let editor, model, document, view, viewDocument;
 	let editor, model, document, view, viewDocument;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		return VirtualTestEditor
 		return VirtualTestEditor
 			.create( {
 			.create( {
-				plugins: [ ImageEngine ]
+				plugins: [ ImageEditing ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
@@ -28,7 +28,7 @@ describe( 'ImageEngine', () => {
 	} );
 	} );
 
 
 	it( 'should be loaded', () => {
 	it( 'should be loaded', () => {
-		expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine );
+		expect( editor.plugins.get( ImageEditing ) ).to.be.instanceOf( ImageEditing );
 	} );
 	} );
 
 
 	it( 'should set proper schema rules', () => {
 	it( 'should set proper schema rules', () => {

+ 3 - 3
packages/ckeditor5-image/tests/imagecaption.js

@@ -7,7 +7,7 @@
 
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ImageCaption from '../src/imagecaption';
 import ImageCaption from '../src/imagecaption';
-import ImageCaptionEngine from '../src/imagecaption/imagecaptionengine';
+import ImageCaptionEditing from '../src/imagecaption/imagecaptionediting';
 
 
 describe( 'ImageCaption', () => {
 describe( 'ImageCaption', () => {
 	let editor;
 	let editor;
@@ -29,7 +29,7 @@ describe( 'ImageCaption', () => {
 		expect( editor.plugins.get( ImageCaption ) ).to.instanceOf( ImageCaption );
 		expect( editor.plugins.get( ImageCaption ) ).to.instanceOf( ImageCaption );
 	} );
 	} );
 
 
-	it( 'should load ImageCaptionEngine plugin', () => {
-		expect( editor.plugins.get( ImageCaptionEngine ) ).to.instanceOf( ImageCaptionEngine );
+	it( 'should load ImageCaptionEditing plugin', () => {
+		expect( editor.plugins.get( ImageCaptionEditing ) ).to.instanceOf( ImageCaptionEditing );
 	} );
 	} );
 } );
 } );

+ 6 - 6
packages/ckeditor5-image/tests/imagecaption/imagecaptionengine.js → packages/ckeditor5-image/tests/imagecaption/imagecaptionediting.js

@@ -5,9 +5,9 @@
 
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 
 
-import ImageCaptionEngine from '../../src/imagecaption/imagecaptionengine';
-import ImageEngine from '../../src/image/imageengine';
-import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
+import ImageCaptionEditing from '../../src/imagecaption/imagecaptionediting';
+import ImageEditing from '../../src/image/imageediting';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 
 import ViewAttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeelement';
 import ViewAttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeelement';
@@ -18,13 +18,13 @@ import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 
-describe( 'ImageCaptionEngine', () => {
+describe( 'ImageCaptionEditing', () => {
 	let editor, model, doc, view;
 	let editor, model, doc, view;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		return VirtualTestEditor
 		return VirtualTestEditor
 			.create( {
 			.create( {
-				plugins: [ ImageCaptionEngine, ImageEngine, UndoEngine, Paragraph ]
+				plugins: [ ImageCaptionEditing, ImageEditing, UndoEditing, Paragraph ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
@@ -44,7 +44,7 @@ describe( 'ImageCaptionEngine', () => {
 	} );
 	} );
 
 
 	it( 'should be loaded', () => {
 	it( 'should be loaded', () => {
-		expect( editor.plugins.get( ImageCaptionEngine ) ).to.be.instanceOf( ImageCaptionEngine );
+		expect( editor.plugins.get( ImageCaptionEditing ) ).to.be.instanceOf( ImageCaptionEditing );
 	} );
 	} );
 
 
 	it( 'should set proper schema rules', () => {
 	it( 'should set proper schema rules', () => {

+ 7 - 52
packages/ckeditor5-image/tests/imagestyle.js

@@ -5,17 +5,12 @@
 
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ImageStyle from '../src/imagestyle';
 import ImageStyle from '../src/imagestyle';
-import ImageStyleEngine from '../src/imagestyle/imagestyleengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import ImageStyleEditing from '../src/imagestyle/imagestyleediting';
+import ImageStyleUI from '../src/imagestyle/imagestyleui';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 
 describe( 'ImageStyle', () => {
 describe( 'ImageStyle', () => {
 	let editor;
 	let editor;
-	const styles = [
-		{ name: 'style 1', title: 'Style 1 title', icon: 'style1-icon', isDefault: true },
-		{ name: 'style 2', title: 'Style 2 title', icon: 'style2-icon', cssClass: 'style2-class' },
-		{ name: 'style 3', title: 'Style 3 title', icon: 'style3-icon', cssClass: 'style3-class' }
-	];
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		const editorElement = global.document.createElement( 'div' );
 		const editorElement = global.document.createElement( 'div' );
@@ -23,10 +18,7 @@ describe( 'ImageStyle', () => {
 
 
 		return ClassicTestEditor
 		return ClassicTestEditor
 			.create( editorElement, {
 			.create( editorElement, {
-				plugins: [ ImageStyle ],
-				image: {
-					styles
-				}
+				plugins: [ ImageStyle ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
@@ -41,48 +33,11 @@ describe( 'ImageStyle', () => {
 		expect( editor.plugins.get( ImageStyle ) ).to.be.instanceOf( ImageStyle );
 		expect( editor.plugins.get( ImageStyle ) ).to.be.instanceOf( ImageStyle );
 	} );
 	} );
 
 
-	it( 'should load ImageStyleEngine plugin', () => {
-		expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine );
+	it( 'should load ImageStyleEditing plugin', () => {
+		expect( editor.plugins.get( ImageStyleEditing ) ).to.be.instanceOf( ImageStyleEditing );
 	} );
 	} );
 
 
-	it( 'should register buttons for each style', () => {
-		const spy = sinon.spy( editor, 'execute' );
-
-		for ( const style of styles ) {
-			const command = editor.commands.get( style.name );
-			const buttonView = editor.ui.componentFactory.create( style.name );
-
-			expect( buttonView ).to.be.instanceOf( ButtonView );
-			expect( buttonView.label ).to.equal( style.title );
-			expect( buttonView.icon ).to.equal( style.icon );
-
-			command.isEnabled = true;
-			expect( buttonView.isEnabled ).to.be.true;
-			command.isEnabled = false;
-			expect( buttonView.isEnabled ).to.be.false;
-
-			buttonView.fire( 'execute' );
-			sinon.assert.calledWithExactly( editor.execute, style.name );
-
-			spy.reset();
-		}
-	} );
-
-	it( 'should not add buttons to image toolbar if configuration is present', () => {
-		const editorElement = global.document.createElement( 'div' );
-		global.document.body.appendChild( editorElement );
-
-		return ClassicTestEditor
-			.create( editorElement, {
-				plugins: [ ImageStyle ],
-				image: {
-					styles,
-					toolbar: [ 'foo', 'bar' ]
-				}
-			} )
-			.then( newEditor => {
-				expect( newEditor.config.get( 'image.toolbar' ) ).to.eql( [ 'foo', 'bar' ] );
-				newEditor.destroy();
-			} );
+	it( 'should load ImageStyleUI plugin', () => {
+		expect( editor.plugins.get( ImageStyleUI ) ).to.be.instanceOf( ImageStyleUI );
 	} );
 	} );
 } );
 } );

+ 15 - 203
packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js → packages/ckeditor5-image/tests/imagestyle/imagestyleediting.js

@@ -4,25 +4,19 @@
  */
  */
 
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageStyleEngine from '../../src/imagestyle/imagestyleengine';
-import ImageEngine from '../../src/image/imageengine';
+import ImageStyleEditing from '../../src/imagestyle/imagestyleediting';
+import ImageEditing from '../../src/image/imageediting';
 import ImageStyleCommand from '../../src/imagestyle/imagestylecommand';
 import ImageStyleCommand from '../../src/imagestyle/imagestylecommand';
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import log from '@ckeditor/ckeditor5-utils/src/log';
+
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 
-import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
-import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
-import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
-
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 
 testUtils.createSinonSandbox();
 testUtils.createSinonSandbox();
 
 
-describe( 'ImageStyleEngine', () => {
-	let editor, plugin, model, document, viewDocument;
+describe( 'ImageStyleEditing', () => {
+	let editor, model, document, viewDocument;
 
 
 	afterEach( () => {
 	afterEach( () => {
 		editor.destroy();
 		editor.destroy();
@@ -32,7 +26,7 @@ describe( 'ImageStyleEngine', () => {
 		beforeEach( () => {
 		beforeEach( () => {
 			return VirtualTestEditor
 			return VirtualTestEditor
 				.create( {
 				.create( {
-					plugins: [ ImageStyleEngine ],
+					plugins: [ ImageStyleEditing ],
 				} )
 				} )
 				.then( newEditor => {
 				.then( newEditor => {
 					editor = newEditor;
 					editor = newEditor;
@@ -40,11 +34,11 @@ describe( 'ImageStyleEngine', () => {
 		} );
 		} );
 
 
 		it( 'should be loaded', () => {
 		it( 'should be loaded', () => {
-			expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine );
+			expect( editor.plugins.get( ImageStyleEditing ) ).to.be.instanceOf( ImageStyleEditing );
 		} );
 		} );
 
 
-		it( 'should load image engine', () => {
-			expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine );
+		it( 'should load image editing', () => {
+			expect( editor.plugins.get( ImageEditing ) ).to.be.instanceOf( ImageEditing );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -52,7 +46,7 @@ describe( 'ImageStyleEngine', () => {
 		beforeEach( () => {
 		beforeEach( () => {
 			return VirtualTestEditor
 			return VirtualTestEditor
 				.create( {
 				.create( {
-					plugins: [ ImageStyleEngine ],
+					plugins: [ ImageStyleEditing ],
 					image: {
 					image: {
 						styles: [
 						styles: [
 							{ name: 'fullStyle', title: 'foo', icon: 'object-center', isDefault: true },
 							{ name: 'fullStyle', title: 'foo', icon: 'object-center', isDefault: true },
@@ -72,7 +66,7 @@ describe( 'ImageStyleEngine', () => {
 		it( 'should define image.styles config', () => {
 		it( 'should define image.styles config', () => {
 			return VirtualTestEditor
 			return VirtualTestEditor
 				.create( {
 				.create( {
-					plugins: [ ImageStyleEngine ]
+					plugins: [ ImageStyleEditing ]
 				} )
 				} )
 				.then( newEditor => {
 				.then( newEditor => {
 					editor = newEditor;
 					editor = newEditor;
@@ -263,11 +257,11 @@ describe( 'ImageStyleEngine', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'imageStyles()', () => {
+	describe( 'config', () => {
 		it( 'should fall back to defaults when no image.styles', () => {
 		it( 'should fall back to defaults when no image.styles', () => {
 			return VirtualTestEditor
 			return VirtualTestEditor
 				.create( {
 				.create( {
-					plugins: [ ImageStyleEngine ]
+					plugins: [ ImageStyleEditing ]
 				} )
 				} )
 				.then( newEditor => {
 				.then( newEditor => {
 					editor = newEditor;
 					editor = newEditor;
@@ -279,7 +273,7 @@ describe( 'ImageStyleEngine', () => {
 		it( 'should not alter the image.styles config', () => {
 		it( 'should not alter the image.styles config', () => {
 			return VirtualTestEditor
 			return VirtualTestEditor
 				.create( {
 				.create( {
-					plugins: [ ImageStyleEngine ],
+					plugins: [ ImageStyleEditing ],
 					image: {
 					image: {
 						styles: [
 						styles: [
 							'imageStyleSide'
 							'imageStyleSide'
@@ -296,7 +290,7 @@ describe( 'ImageStyleEngine', () => {
 		it( 'should not alter object definitions in the image.styles config', () => {
 		it( 'should not alter object definitions in the image.styles config', () => {
 			return VirtualTestEditor
 			return VirtualTestEditor
 				.create( {
 				.create( {
-					plugins: [ ImageStyleEngine ],
+					plugins: [ ImageStyleEditing ],
 					image: {
 					image: {
 						styles: [
 						styles: [
 							{ name: 'imageStyleSide' }
 							{ name: 'imageStyleSide' }
@@ -309,187 +303,5 @@ describe( 'ImageStyleEngine', () => {
 					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ { name: 'imageStyleSide' } ] );
 					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ { name: 'imageStyleSide' } ] );
 				} );
 				} );
 		} );
 		} );
-
-		it( 'should cache the styles', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ]
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-					plugin = editor.plugins.get( ImageStyleEngine );
-
-					expect( plugin.imageStyles ).to.equal( plugin.imageStyles );
-				} );
-		} );
-
-		describe( 'object format', () => {
-			beforeEach( () => {
-				class TranslationMock extends Plugin {
-					init() {
-						sinon.stub( this.editor, 't' ).returns( 'Default translation' );
-					}
-				}
-
-				return VirtualTestEditor
-					.create( {
-						plugins: [ TranslationMock, ImageStyleEngine ],
-						image: {
-							styles: [
-								// Custom user styles.
-								{ name: 'foo', title: 'foo', icon: 'custom', isDefault: true, className: 'foo-class' },
-								{ name: 'bar', title: 'bar', icon: 'right', className: 'bar-class' },
-								{ name: 'baz', title: 'Side image', icon: 'custom', className: 'baz-class' },
-
-								// Customized default styles.
-								{ name: 'imageStyleFull', icon: 'left', title: 'Custom title' }
-							]
-						}
-					} )
-					.then( newEditor => {
-						editor = newEditor;
-						plugin = editor.plugins.get( ImageStyleEngine );
-					} );
-			} );
-
-			it( 'should pass through if #name not found in default styles', () => {
-				expect( plugin.imageStyles[ 0 ] ).to.deep.equal( {
-					name: 'foo',
-					title: 'foo',
-					icon: 'custom',
-					isDefault: true,
-					className: 'foo-class'
-				} );
-			} );
-
-			it( 'should use one of default icons if #icon matches', () => {
-				expect( plugin.imageStyles[ 1 ].icon ).to.equal( ImageStyleEngine.defaultIcons.right );
-			} );
-
-			it( 'should use one of default translations if #title matches', () => {
-				expect( plugin.imageStyles[ 2 ].title ).to.deep.equal( 'Default translation' );
-			} );
-
-			it( 'should extend one of default styles if #name matches', () => {
-				expect( plugin.imageStyles[ 3 ] ).to.deep.equal( {
-					name: 'imageStyleFull',
-					title: 'Custom title',
-					icon: ImageStyleEngine.defaultIcons.left,
-					isDefault: true
-				} );
-			} );
-		} );
-
-		describe( 'string format', () => {
-			it( 'should use one of default styles if #name matches', () => {
-				return VirtualTestEditor
-					.create( {
-						plugins: [ ImageStyleEngine ],
-						image: {
-							styles: [ 'imageStyleFull' ]
-						}
-					} )
-					.then( newEditor => {
-						editor = newEditor;
-						plugin = editor.plugins.get( ImageStyleEngine );
-						expect( plugin.imageStyles[ 0 ] ).to.deep.equal( ImageStyleEngine.defaultStyles.imageStyleFull );
-					} );
-			} );
-
-			it( 'should warn if a #name not found in default styles', () => {
-				testUtils.sinon.stub( log, 'warn' );
-
-				return VirtualTestEditor
-					.create( {
-						plugins: [ ImageStyleEngine ],
-						image: {
-							styles: [ 'foo' ]
-						}
-					} )
-					.then( newEditor => {
-						editor = newEditor;
-						plugin = editor.plugins.get( ImageStyleEngine );
-
-						expect( plugin.imageStyles[ 0 ] ).to.deep.equal( {
-							name: 'foo'
-						} );
-
-						sinon.assert.calledOnce( log.warn );
-						sinon.assert.calledWithExactly( log.warn,
-							sinon.match( /^image-style-not-found/ ),
-							{ name: 'foo' }
-						);
-					} );
-			} );
-		} );
-	} );
-
-	describe( 'localizedDefaultStylesTitles()', () => {
-		it( 'should return localized titles of default styles', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ]
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-					plugin = editor.plugins.get( ImageStyleEngine );
-
-					expect( plugin.localizedDefaultStylesTitles ).to.deep.equal( {
-						'Full size image': 'Full size image',
-						'Side image': 'Side image',
-						'Left aligned image': 'Left aligned image',
-						'Centered image': 'Centered image',
-						'Right aligned image': 'Right aligned image'
-					} );
-				} );
-		} );
-	} );
-
-	describe( 'defaultStyles', () => {
-		it( 'should be defined', () => {
-			expect( ImageStyleEngine.defaultStyles ).to.deep.equal( {
-				imageStyleFull: {
-					name: 'imageStyleFull',
-					title: 'Full size image',
-					icon: fullWidthIcon,
-					isDefault: true
-				},
-				imageStyleSide: {
-					name: 'imageStyleSide',
-					title: 'Side image',
-					icon: rightIcon,
-					className: 'image-style-side'
-				},
-				imageStyleAlignLeft: {
-					name: 'imageStyleAlignLeft',
-					title: 'Left aligned image',
-					icon: leftIcon,
-					className: 'image-style-align-left'
-				},
-				imageStyleAlignCenter: {
-					name: 'imageStyleAlignCenter',
-					title: 'Centered image',
-					icon: centerIcon,
-					className: 'image-style-align-center'
-				},
-				imageStyleAlignRight: {
-					name: 'imageStyleAlignRight',
-					title: 'Right aligned image',
-					icon: rightIcon,
-					className: 'image-style-align-right'
-				}
-			} );
-		} );
-	} );
-
-	describe( 'defaultIcons', () => {
-		it( 'should be defined', () => {
-			expect( ImageStyleEngine.defaultIcons ).to.deep.equal( {
-				full: fullWidthIcon,
-				left: leftIcon,
-				right: rightIcon,
-				center: centerIcon,
-			} );
-		} );
 	} );
 	} );
 } );
 } );

+ 134 - 0
packages/ckeditor5-image/tests/imagestyle/imagestyleui.js

@@ -0,0 +1,134 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import ImageStyleEditing from '../../src/imagestyle/imagestyleediting';
+import ImageStyleUI from '../../src/imagestyle/imagestyleui';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+
+describe( 'ImageStyleUI', () => {
+	let editor;
+
+	const styles = [
+		{ name: 'style 1', title: 'Style 1 title', icon: 'style1-icon', isDefault: true },
+		{ name: 'style 2', title: 'Style 2 title', icon: 'style2-icon', cssClass: 'style2-class' },
+		{ name: 'style 3', title: 'Style 3 title', icon: 'style3-icon', cssClass: 'style3-class' }
+	];
+
+	beforeEach( () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ ImageStyleEditing, ImageStyleUI ],
+				image: {
+					styles
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should register buttons for each style', () => {
+		const spy = sinon.spy( editor, 'execute' );
+
+		for ( const style of styles ) {
+			const command = editor.commands.get( style.name );
+			const buttonView = editor.ui.componentFactory.create( style.name );
+
+			expect( buttonView ).to.be.instanceOf( ButtonView );
+			expect( buttonView.label ).to.equal( style.title );
+			expect( buttonView.icon ).to.equal( style.icon );
+
+			command.isEnabled = true;
+			expect( buttonView.isEnabled ).to.be.true;
+			command.isEnabled = false;
+			expect( buttonView.isEnabled ).to.be.false;
+
+			buttonView.fire( 'execute' );
+			sinon.assert.calledWithExactly( editor.execute, style.name );
+
+			spy.reset();
+		}
+	} );
+
+	it( 'should translate buttons if taken from default styles', () => {
+		const editorElement = global.document.createElement( 'div' );
+
+		global.document.body.appendChild( editorElement );
+
+		class TranslationMock extends Plugin {
+			init() {
+				sinon.stub( this.editor, 't' ).returns( 'Default title' );
+			}
+		}
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ TranslationMock, ImageStyleEditing, ImageStyleUI ],
+				image: {
+					styles: [
+						{ name: 'style 1', title: 'Side image', icon: 'style1-icon', isDefault: true }
+					]
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+
+				const buttonView = editor.ui.componentFactory.create( 'style 1' );
+
+				expect( buttonView.label ).to.equal( 'Default title' );
+			} );
+	} );
+
+	it( 'should not add buttons to image toolbar if configuration is present', () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ ImageStyleEditing, ImageStyleUI ],
+				image: {
+					styles,
+					toolbar: [ 'foo', 'bar' ]
+				}
+			} )
+			.then( newEditor => {
+				expect( newEditor.config.get( 'image.toolbar' ) ).to.deep.equal( [ 'foo', 'bar' ] );
+				newEditor.destroy();
+			} );
+	} );
+
+	describe( 'localizedDefaultStylesTitles()', () => {
+		it( 'should return localized titles of default styles', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleUI ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					const plugin = editor.plugins.get( ImageStyleUI );
+
+					expect( plugin.localizedDefaultStylesTitles ).to.deep.equal( {
+						'Full size image': 'Full size image',
+						'Side image': 'Side image',
+						'Left aligned image': 'Left aligned image',
+						'Centered image': 'Centered image',
+						'Right aligned image': 'Right aligned image'
+					} );
+				} );
+		} );
+	} );
+} );

+ 111 - 0
packages/ckeditor5-image/tests/imagestyle/utils.js

@@ -0,0 +1,111 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
+import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
+import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
+import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { normalizeImageStyles } from '../../src/imagestyle/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'ImageStyle utils', () => {
+	let imageStyles;
+
+	describe( 'imageStyles()', () => {
+		describe( 'object format', () => {
+			beforeEach( () => {
+				imageStyles = normalizeImageStyles( [
+					{ name: 'foo', title: 'foo', icon: 'custom', isDefault: true, className: 'foo-class' },
+					{ name: 'bar', title: 'bar', icon: 'right', className: 'bar-class' },
+					{ name: 'baz', title: 'Side image', icon: 'custom', className: 'baz-class' },
+
+					// Customized default styles.
+					{ name: 'imageStyleFull', icon: 'left', title: 'Custom title' }
+				] );
+			} );
+
+			it( 'should pass through if #name not found in default styles', () => {
+				expect( imageStyles[ 0 ] ).to.deep.equal( {
+					name: 'foo',
+					title: 'foo',
+					icon: 'custom',
+					isDefault: true,
+					className: 'foo-class'
+				} );
+			} );
+
+			it( 'should use one of default icons if #icon matches', () => {
+				expect( imageStyles[ 1 ].icon ).to.equal( rightIcon );
+			} );
+
+			it( 'should extend one of default styles if #name matches', () => {
+				expect( imageStyles[ 3 ] ).to.deep.equal( {
+					name: 'imageStyleFull',
+					title: 'Custom title',
+					icon: leftIcon,
+					isDefault: true
+				} );
+			} );
+		} );
+
+		describe( 'string format', () => {
+			it( 'should use one of default styles if #name matches', () => {
+				expect( normalizeImageStyles( [ 'imageStyleFull' ] ) ).to.deep.equal( [ {
+					name: 'imageStyleFull',
+					title: 'Full size image',
+					icon: fullWidthIcon,
+					isDefault: true
+				} ] );
+
+				expect( normalizeImageStyles( [ 'imageStyleSide' ] ) ).to.deep.equal( [ {
+					name: 'imageStyleSide',
+					title: 'Side image',
+					icon: rightIcon,
+					className: 'image-style-side'
+				} ] );
+
+				expect( normalizeImageStyles( [ 'imageStyleAlignLeft' ] ) ).to.deep.equal( [ {
+					name: 'imageStyleAlignLeft',
+					title: 'Left aligned image',
+					icon: leftIcon,
+					className: 'image-style-align-left'
+				} ] );
+
+				expect( normalizeImageStyles( [ 'imageStyleAlignCenter' ] ) ).to.deep.equal( [ {
+					name: 'imageStyleAlignCenter',
+					title: 'Centered image',
+					icon: centerIcon,
+					className: 'image-style-align-center'
+				} ] );
+
+				expect( normalizeImageStyles( [ 'imageStyleAlignRight' ] ) ).to.deep.equal( [ {
+					name: 'imageStyleAlignRight',
+					title: 'Right aligned image',
+					icon: rightIcon,
+					className: 'image-style-align-right'
+				} ] );
+			} );
+
+			it( 'should warn if a #name not found in default styles', () => {
+				testUtils.sinon.stub( log, 'warn' );
+
+				expect( normalizeImageStyles( [ 'foo' ] ) ).to.deep.equal( [ {
+					name: 'foo'
+				} ] );
+
+				sinon.assert.calledOnce( log.warn );
+				sinon.assert.calledWithExactly( log.warn,
+					sinon.match( /^image-style-not-found/ ),
+					{ name: 'foo' }
+				);
+			} );
+		} );
+	} );
+} );

+ 7 - 196
packages/ckeditor5-image/tests/imagetextalternative.js

@@ -3,20 +3,15 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* global Event, document */
-
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Image from '../src/image';
 import Image from '../src/image';
 import ImageTextAlternative from '../src/imagetextalternative';
 import ImageTextAlternative from '../src/imagetextalternative';
-import ImageTextAlternativeEngine from '../src/imagetextalternative/imagetextalternativeengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import View from '@ckeditor/ckeditor5-ui/src/view';
+import ImageTextAlternativeEditing from '../src/imagetextalternative/imagetextalternativeediting';
+import ImageTextAlternativeUI from '../src/imagetextalternative/imagetextalternativeui';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 
 describe( 'ImageTextAlternative', () => {
 describe( 'ImageTextAlternative', () => {
-	let editor, model, view, doc, plugin, command, form, balloon, editorElement, button;
+	let editor, plugin, editorElement;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		editorElement = global.document.createElement( 'div' );
 		editorElement = global.document.createElement( 'div' );
@@ -28,15 +23,8 @@ describe( 'ImageTextAlternative', () => {
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
-				model = editor.model;
-				view = editor.editing.view;
-				doc = model.document;
 				newEditor.editing.view.attachDomRoot( editorElement );
 				newEditor.editing.view.attachDomRoot( editorElement );
 				plugin = editor.plugins.get( ImageTextAlternative );
 				plugin = editor.plugins.get( ImageTextAlternative );
-				command = editor.commands.get( 'imageTextAlternative' );
-				form = plugin._form;
-				balloon = editor.plugins.get( 'ContextualBalloon' );
-				button = editor.ui.componentFactory.create( 'imageTextAlternative' );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -50,188 +38,11 @@ describe( 'ImageTextAlternative', () => {
 		expect( plugin ).to.be.instanceOf( ImageTextAlternative );
 		expect( plugin ).to.be.instanceOf( ImageTextAlternative );
 	} );
 	} );
 
 
-	it( 'should load ImageTextAlternativeEngine plugin', () => {
-		expect( editor.plugins.get( ImageTextAlternativeEngine ) ).to.be.instanceOf( ImageTextAlternativeEngine );
-	} );
-
-	describe( 'toolbar button', () => {
-		it( 'should be registered in component factory', () => {
-			expect( button ).to.be.instanceOf( ButtonView );
-		} );
-
-		it( 'should have isEnabled property bind to command\'s isEnabled property', () => {
-			command.isEnabled = true;
-			expect( button.isEnabled ).to.be.true;
-
-			command.isEnabled = false;
-			expect( button.isEnabled ).to.be.false;
-		} );
-
-		it( 'should show balloon panel on execute', () => {
-			expect( balloon.visibleView ).to.be.null;
-
-			setData( model, '[<image src="" alt="foo bar"></image>]' );
-
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-
-			// Make sure successive execute does not throw, e.g. attempting
-			// to display the form twice.
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-		} );
-
-		it( 'should set alt attribute value to textarea and select it', () => {
-			const spy = sinon.spy( form.labeledInput, 'select' );
-
-			setData( model, '[<image src="" alt="foo bar"></image>]' );
-
-			button.fire( 'execute' );
-			sinon.assert.calledOnce( spy );
-			expect( form.labeledInput.value ).equals( 'foo bar' );
-		} );
-
-		it( 'should set empty text to textarea and select it when there is no alt attribute', () => {
-			const spy = sinon.spy( form.labeledInput, 'select' );
-
-			setData( model, '[<image src=""></image>]' );
-
-			button.fire( 'execute' );
-			sinon.assert.calledOnce( spy );
-			expect( form.labeledInput.value ).equals( '' );
-		} );
+	it( 'should load ImageTextAlternativeEditing plugin', () => {
+		expect( editor.plugins.get( ImageTextAlternativeEditing ) ).to.be.instanceOf( ImageTextAlternativeEditing );
 	} );
 	} );
 
 
-	describe( 'balloon panel form', () => {
-		// https://github.com/ckeditor/ckeditor5-image/issues/114
-		it( 'should make sure the input always stays in sync with the value of the command', () => {
-			const button = editor.ui.componentFactory.create( 'imageTextAlternative' );
-
-			// Mock the value of the input after some past editing.
-			form.labeledInput.value = 'foo';
-
-			// Mock the user using the form, changing the value but clicking "Cancel".
-			// so the command's value is not updated.
-			form.labeledInput.inputView.element.value = 'This value was canceled.';
-
-			// Mock the user editing the same image once again.
-			setData( model, '[<image src="" alt="foo"></image>]' );
-
-			button.fire( 'execute' );
-			expect( form.labeledInput.inputView.element.value ).to.equal( 'foo' );
-		} );
-
-		it( 'should execute command on submit', () => {
-			const spy = sinon.spy( editor, 'execute' );
-			form.fire( 'submit' );
-
-			sinon.assert.calledOnce( spy );
-			sinon.assert.calledWithExactly( spy, 'imageTextAlternative', {
-				newValue: form.labeledInput.inputView.element.value
-			} );
-		} );
-
-		it( 'should hide the panel on cancel and focus the editing view', () => {
-			const spy = sinon.spy( editor.editing.view, 'focus' );
-
-			setData( model, '[<image src="" alt="foo bar"></image>]' );
-
-			editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-
-			form.fire( 'cancel' );
-			expect( balloon.visibleView ).to.be.null;
-			sinon.assert.calledOnce( spy );
-		} );
-
-		it( 'should not engage when the form is in the balloon yet invisible', () => {
-			setData( model, '[<image src=""></image>]' );
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-
-			const lastView = new View();
-			lastView.element = document.createElement( 'div' );
-
-			balloon.add( { view: lastView } );
-			expect( balloon.visibleView ).to.equal( lastView );
-
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( lastView );
-		} );
-
-		describe( 'integration with the editor selection (#render event)', () => {
-			it( 'should re-position the form', () => {
-				setData( model, '[<image src=""></image>]' );
-				button.fire( 'execute' );
-
-				const spy = sinon.spy( balloon, 'updatePosition' );
-
-				view.fire( 'render' );
-				sinon.assert.calledOnce( spy );
-			} );
-
-			it( 'should hide the form and focus editable when image widget has been removed by external change', () => {
-				setData( model, '[<image src=""></image>]' );
-				button.fire( 'execute' );
-
-				const removeSpy = sinon.spy( balloon, 'remove' );
-				const focusSpy = sinon.spy( editor.editing.view, 'focus' );
-
-				// EnqueueChange automatically fires #render event.
-				model.enqueueChange( 'transparent', writer => {
-					writer.remove( doc.selection.getFirstRange() );
-				} );
-
-				sinon.assert.calledWithExactly( removeSpy, form );
-				sinon.assert.calledOnce( focusSpy );
-			} );
-		} );
-
-		describe( 'close listeners', () => {
-			describe( 'keyboard', () => {
-				it( 'should close upon Esc key press and focus the editing view', () => {
-					const hideSpy = sinon.spy( plugin, '_hideForm' );
-					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
-
-					setData( model, '[<image src=""></image>]' );
-					button.fire( 'execute' );
-
-					const keyEvtData = {
-						keyCode: keyCodes.esc,
-						preventDefault: sinon.spy(),
-						stopPropagation: sinon.spy()
-					};
-
-					form.keystrokes.press( keyEvtData );
-					sinon.assert.calledOnce( hideSpy );
-					sinon.assert.calledOnce( keyEvtData.preventDefault );
-					sinon.assert.calledOnce( focusSpy );
-				} );
-			} );
-
-			describe( 'mouse', () => {
-				it( 'should close and not focus editable on click outside the panel', () => {
-					const hideSpy = sinon.spy( plugin, '_hideForm' );
-					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
-
-					setData( model, '[<image src=""></image>]' );
-					button.fire( 'execute' );
-
-					global.document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
-					sinon.assert.called( hideSpy );
-					sinon.assert.notCalled( focusSpy );
-				} );
-
-				it( 'should not close on click inside the panel', () => {
-					const spy = sinon.spy( plugin, '_hideForm' );
-
-					setData( model, '[<image src=""></image>]' );
-					button.fire( 'execute' );
-
-					form.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
-					sinon.assert.notCalled( spy );
-				} );
-			} );
-		} );
+	it( 'should load ImageTextAlternativeUI plugin', () => {
+		expect( editor.plugins.get( ImageTextAlternativeUI ) ).to.be.instanceOf( ImageTextAlternativeUI );
 	} );
 	} );
 } );
 } );

+ 5 - 4
packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeengine.js → packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeediting.js

@@ -4,22 +4,23 @@
  */
  */
 
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageTextAlternativeEngine from '../../src/imagetextalternative/imagetextalternativeengine';
+import ImageTextAlternativeEditing from '../../src/imagetextalternative/imagetextalternativeediting';
 import ImageTextAlternativeCommand from '../../src/imagetextalternative/imagetextalternativecommand';
 import ImageTextAlternativeCommand from '../../src/imagetextalternative/imagetextalternativecommand';
 
 
-describe( 'ImageTextAlternativeEngine', () => {
+describe( 'ImageTextAlternativeEditing', () => {
 	let editor;
 	let editor;
+
 	beforeEach( () => {
 	beforeEach( () => {
 		return VirtualTestEditor
 		return VirtualTestEditor
 			.create( {
 			.create( {
-				plugins: [ ImageTextAlternativeEngine ]
+				plugins: [ ImageTextAlternativeEditing ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
 			} );
 			} );
 	} );
 	} );
 
 
-	it( 'should register ImageAlteranteTextCommand', () => {
+	it( 'should register ImageAlternativeTextCommand', () => {
 		expect( editor.commands.get( 'imageTextAlternative' ) ).to.be.instanceOf( ImageTextAlternativeCommand );
 		expect( editor.commands.get( 'imageTextAlternative' ) ).to.be.instanceOf( ImageTextAlternativeCommand );
 	} );
 	} );
 } );
 } );

+ 228 - 0
packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js

@@ -0,0 +1,228 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global Event, document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Image from '../../src/image';
+import ImageTextAlternativeEditing from '../../src/imagetextalternative/imagetextalternativeediting';
+import ImageTextAlternativeUI from '../../src/imagetextalternative/imagetextalternativeui';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+
+describe( 'ImageTextAlternative', () => {
+	let editor, model, doc, plugin, command, form, balloon, editorElement, button;
+
+	beforeEach( () => {
+		editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ ImageTextAlternativeEditing, ImageTextAlternativeUI, Image ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+				newEditor.editing.view.attachDomRoot( editorElement );
+				plugin = editor.plugins.get( ImageTextAlternativeUI );
+				command = editor.commands.get( 'imageTextAlternative' );
+				form = plugin._form;
+				balloon = editor.plugins.get( 'ContextualBalloon' );
+				button = editor.ui.componentFactory.create( 'imageTextAlternative' );
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'toolbar button', () => {
+		it( 'should be registered in component factory', () => {
+			expect( button ).to.be.instanceOf( ButtonView );
+		} );
+
+		it( 'should have isEnabled property bind to command\'s isEnabled property', () => {
+			command.isEnabled = true;
+			expect( button.isEnabled ).to.be.true;
+
+			command.isEnabled = false;
+			expect( button.isEnabled ).to.be.false;
+		} );
+
+		it( 'should show balloon panel on execute', () => {
+			expect( balloon.visibleView ).to.be.null;
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+
+			// Make sure successive execute does not throw, e.g. attempting
+			// to display the form twice.
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+		} );
+
+		it( 'should set alt attribute value to textarea and select it', () => {
+			const spy = sinon.spy( form.labeledInput, 'select' );
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			button.fire( 'execute' );
+			sinon.assert.calledOnce( spy );
+			expect( form.labeledInput.value ).equals( 'foo bar' );
+		} );
+
+		it( 'should set empty text to textarea and select it when there is no alt attribute', () => {
+			const spy = sinon.spy( form.labeledInput, 'select' );
+
+			setData( model, '[<image src=""></image>]' );
+
+			button.fire( 'execute' );
+			sinon.assert.calledOnce( spy );
+			expect( form.labeledInput.value ).equals( '' );
+		} );
+	} );
+
+	describe( 'balloon panel form', () => {
+		// https://github.com/ckeditor/ckeditor5-image/issues/114
+		it( 'should make sure the input always stays in sync with the value of the command', () => {
+			const button = editor.ui.componentFactory.create( 'imageTextAlternative' );
+
+			// Mock the value of the input after some past editing.
+			form.labeledInput.value = 'foo';
+
+			// Mock the user using the form, changing the value but clicking "Cancel".
+			// so the command's value is not updated.
+			form.labeledInput.inputView.element.value = 'This value was canceled.';
+
+			// Mock the user editing the same image once again.
+			setData( model, '[<image src="" alt="foo"></image>]' );
+
+			button.fire( 'execute' );
+			expect( form.labeledInput.inputView.element.value ).to.equal( 'foo' );
+		} );
+
+		it( 'should execute command on submit', () => {
+			const spy = sinon.spy( editor, 'execute' );
+			form.fire( 'submit' );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, 'imageTextAlternative', {
+				newValue: form.labeledInput.inputView.element.value
+			} );
+		} );
+
+		it( 'should hide the panel on cancel and focus the editing view', () => {
+			const spy = sinon.spy( editor.editing.view, 'focus' );
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+
+			form.fire( 'cancel' );
+			expect( balloon.visibleView ).to.be.null;
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should not engage when the form is in the balloon yet invisible', () => {
+			setData( model, '[<image src=""></image>]' );
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+
+			const lastView = new View();
+			lastView.element = document.createElement( 'div' );
+
+			balloon.add( { view: lastView } );
+			expect( balloon.visibleView ).to.equal( lastView );
+
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( lastView );
+		} );
+
+		describe( 'integration with the editor selection (#render event)', () => {
+			it( 'should re-position the form', () => {
+				setData( model, '[<image src=""></image>]' );
+				button.fire( 'execute' );
+
+				const spy = sinon.spy( balloon, 'updatePosition' );
+
+				editor.editing.view.fire( 'render' );
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should hide the form and focus editable when image widget has been removed by external change', () => {
+				setData( model, '[<image src=""></image>]' );
+				button.fire( 'execute' );
+
+				const removeSpy = sinon.spy( balloon, 'remove' );
+				const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+				// EnqueueChange automatically fires #render event.
+				model.enqueueChange( 'transparent', writer => {
+					writer.remove( doc.selection.getFirstRange() );
+				} );
+
+				sinon.assert.calledWithExactly( removeSpy, form );
+				sinon.assert.calledOnce( focusSpy );
+			} );
+		} );
+
+		describe( 'close listeners', () => {
+			describe( 'keyboard', () => {
+				it( 'should close upon Esc key press and focus the editing view', () => {
+					const hideSpy = sinon.spy( plugin, '_hideForm' );
+					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+					setData( model, '[<image src=""></image>]' );
+					button.fire( 'execute' );
+
+					const keyEvtData = {
+						keyCode: keyCodes.esc,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+
+					form.keystrokes.press( keyEvtData );
+					sinon.assert.calledOnce( hideSpy );
+					sinon.assert.calledOnce( keyEvtData.preventDefault );
+					sinon.assert.calledOnce( focusSpy );
+				} );
+			} );
+
+			describe( 'mouse', () => {
+				it( 'should close and not focus editable on click outside the panel', () => {
+					const hideSpy = sinon.spy( plugin, '_hideForm' );
+					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+					setData( model, '[<image src=""></image>]' );
+					button.fire( 'execute' );
+
+					global.document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
+					sinon.assert.called( hideSpy );
+					sinon.assert.notCalled( focusSpy );
+				} );
+
+				it( 'should not close on click inside the panel', () => {
+					const spy = sinon.spy( plugin, '_hideForm' );
+
+					setData( model, '[<image src=""></image>]' );
+					button.fire( 'execute' );
+
+					form.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
+					sinon.assert.notCalled( spy );
+				} );
+			} );
+		} );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-image/tests/imageupload/imageuploadcommand.js

@@ -12,7 +12,7 @@ import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 
 
 import { createNativeFileMock, UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
 import { createNativeFileMock, UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import Image from '../../src/image/imageengine';
+import Image from '../../src/image/imageediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
 import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';

+ 3 - 3
packages/ckeditor5-image/tests/imageupload/imageuploadediting.js

@@ -8,11 +8,11 @@
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageEngine from '../../src/image/imageengine';
+import ImageEditing from '../../src/image/imageediting';
 import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
 import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import DataTransfer from '@ckeditor/ckeditor5-clipboard/src/datatransfer';
 import DataTransfer from '@ckeditor/ckeditor5-clipboard/src/datatransfer';
 
 
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
@@ -54,7 +54,7 @@ describe( 'ImageUploadEditing', () => {
 
 
 		return VirtualTestEditor
 		return VirtualTestEditor
 			.create( {
 			.create( {
-				plugins: [ ImageEngine, ImageUploadEditing, Paragraph, UndoEngine, UploadAdapterPluginMock ]
+				plugins: [ ImageEditing, ImageUploadEditing, Paragraph, UndoEditing, UploadAdapterPluginMock ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;

+ 2 - 2
packages/ckeditor5-image/tests/imageupload/imageuploadprogress.js

@@ -8,7 +8,7 @@
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageEngine from '../../src/image/imageengine';
+import ImageEditing from '../../src/image/imageediting';
 import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import ImageUploadProgress from '../../src/imageupload/imageuploadprogress';
 import ImageUploadProgress from '../../src/imageupload/imageuploadprogress';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
@@ -50,7 +50,7 @@ describe( 'ImageUploadProgress', () => {
 
 
 		return VirtualTestEditor
 		return VirtualTestEditor
 			.create( {
 			.create( {
-				plugins: [ ImageEngine, Paragraph, ImageUploadEditing, ImageUploadProgress, UploadAdapterPluginMock ]
+				plugins: [ ImageEditing, Paragraph, ImageUploadEditing, ImageUploadProgress, UploadAdapterPluginMock ]
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;

+ 3 - 2
packages/ckeditor5-image/tests/integration.js

@@ -9,9 +9,10 @@ import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloonto
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import Image from '../src/image';
 import Image from '../src/image';
+import ImageToolbar from '../src/imagetoolbar';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
 
-describe( 'Image integration', () => {
+describe( 'ImageToolbar integration', () => {
 	describe( 'with the BalloonToolbar', () => {
 	describe( 'with the BalloonToolbar', () => {
 		let balloon, balloonToolbar, newEditor, editorElement;
 		let balloon, balloonToolbar, newEditor, editorElement;
 
 
@@ -21,7 +22,7 @@ describe( 'Image integration', () => {
 
 
 			return ClassicTestEditor
 			return ClassicTestEditor
 				.create( editorElement, {
 				.create( editorElement, {
-					plugins: [ Image, BalloonToolbar, Paragraph ]
+					plugins: [ Image, ImageToolbar, BalloonToolbar, Paragraph ]
 				} )
 				} )
 				.then( editor => {
 				.then( editor => {
 					newEditor = editor;
 					newEditor = editor;

+ 2 - 2
packages/ckeditor5-image/tests/manual/imageplaceholder.js

@@ -6,11 +6,11 @@
 /* global document */
 /* global document */
 
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageEngine from '../../src/image/imageengine';
+import ImageEditing from '../../src/image/imageediting';
 import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import ImageUploadProgress from '../../src/imageupload/imageuploadprogress';
 import ImageUploadProgress from '../../src/imageupload/imageuploadprogress';
 
 
-VirtualTestEditor.create( { plugins: [ ImageEngine, ImageUploadEditing, ImageUploadProgress ] } )
+VirtualTestEditor.create( { plugins: [ ImageEditing, ImageUploadEditing, ImageUploadProgress ] } )
 	.then( editor => {
 	.then( editor => {
 		const imageUploadProgress = editor.plugins.get( ImageUploadProgress );
 		const imageUploadProgress = editor.plugins.get( ImageUploadProgress );
 		const img = document.createElement( 'img' );
 		const img = document.createElement( 'img' );