Przeglądaj źródła

Merge branch 't/18' into t/24

Szymon Kupś 9 lat temu
rodzic
commit
828167aa98

+ 91 - 42
packages/ckeditor5-image/src/imagestyle/converters.js

@@ -7,16 +7,16 @@
  * @module image/imagestyle/converters
  */
 
-import { isImage, getStyleByValue } from './utils';
+import { isImage } from '../utils';
 
 /**
- * Returns converter for `imageStyle` attribute. It can be used for adding, changing and removing the attribute.
+ * Returns converter for the `imageStyle` attribute. It can be used for adding, changing and removing the attribute.
  *
  * @param {Object} styles Object containing available styles. See {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}
  * for more details.
  * @returns {Function} Model to view attribute converter.
  */
-export function modelToViewSetStyle( styles ) {
+export function modelToViewStyleAttribute( styles ) {
 	return ( evt, data, consumable, conversionApi ) => {
 		const eventType = evt.name.split( ':' )[ 0 ];
 		const consumableType = eventType + ':imageStyle';
@@ -30,57 +30,106 @@ export function modelToViewSetStyle( styles ) {
 		const oldStyle = getStyleByValue( data.attributeOldValue, styles );
 		const viewElement = conversionApi.mapper.toViewElement( data.item );
 
-		if ( eventType == 'changeAttribute' || eventType == 'removeAttribute' ) {
-			if ( !oldStyle ) {
-				return;
-			}
-
-			viewElement.removeClass( oldStyle.className );
+		if ( handleRemoval( eventType, oldStyle, viewElement ) || handleAddition( eventType, newStyle, viewElement ) ) {
+			consumable.consume( data.item, consumableType );
 		}
-
-		if ( eventType == 'addAttribute' || eventType == 'changeAttribute' ) {
-			if ( !newStyle ) {
-				return;
-			}
-
-			viewElement.addClass( newStyle.className );
-		}
-
-		consumable.consume( data.item, consumableType );
 	};
 }
 
 /**
- * Returns view to model converter converting image style CSS class to proper value in the model.
+ * Returns view to model converter converting image CSS classes to proper value in the model.
  *
- * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style Style for which converter is created.
+ * @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>} styles Styles for which converter is created.
  * @returns {Function} View to model converter.
  */
-export function viewToModelImageStyle( style ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewFigureElement = data.input;
-		const modelImageElement = data.output;
+export function viewToModelStyleAttribute( styles ) {
+	// Convert only styles without `null` value.
+	const filteredStyles = styles.filter( style => style.value !== null );
 
-		// *** Step 1: Validate conversion.
-		// Check if view element has proper class to consume.
-		if ( !consumable.test( viewFigureElement, { class: style.className } ) ) {
-			return;
-		}
-
-		// Check if figure is converted to image.
-		if ( !isImage( modelImageElement ) ) {
-			return;
+	return ( evt, data, consumable, conversionApi ) => {
+		for ( let style of filteredStyles ) {
+			viewToModelImageStyle( style, data, consumable, conversionApi );
 		}
+	};
+}
 
-		// Check if image element can be placed in current context wit additional attribute.
-		const attributes = [ ...modelImageElement.getAttributeKeys(), 'imageStyle' ];
+// Converter from view to model converting single style.
+// For more information see {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher};
+//
+// @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
+// @param {Object} data
+// @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable
+// @param {Object} conversionApi
+function viewToModelImageStyle( style, data, consumable, conversionApi ) {
+	const viewFigureElement = data.input;
+	const modelImageElement = data.output;
+
+	// *** Step 1: Validate conversion.
+	// Check if view element has proper class to consume.
+	if ( !consumable.test( viewFigureElement, { class: style.className } ) ) {
+		return;
+	}
+
+	// Check if figure is converted to image.
+	if ( !isImage( modelImageElement ) ) {
+		return;
+	}
+
+	// Check if image element can be placed in current context wit additional attribute.
+	const attributes = [ ...modelImageElement.getAttributeKeys(), 'imageStyle' ];
+
+	if ( !conversionApi.schema.check( { name: 'image', inside: data.context, attributes } ) ) {
+		return;
+	}
+
+	// *** Step2: Convert to model.
+	consumable.consume( viewFigureElement, { class: style.className } );
+	modelImageElement.setAttribute( 'imageStyle', style.value );
+}
 
-		if ( !conversionApi.schema.check( { name: 'image', inside: data.context, attributes } ) ) {
-			return;
+// Returns style with given `value` from array of styles.
+//
+// @param {String} value
+// @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat> } styles
+// @return {module:image/imagestyle/imagestyleengine~ImageStyleFormat|undefined}
+function getStyleByValue( value, styles ) {
+	for ( let style of styles ) {
+		if ( style.value === value ) {
+			return style;
 		}
+	}
+}
 
-		// *** Step2: Convert to model.
-		consumable.consume( viewFigureElement, { class: style.className } );
-		modelImageElement.setAttribute( 'imageStyle', style.value );
-	};
+// Handles converting removal of the attribute.
+// Returns `true` when handling was processed correctly and further conversion can be performed.
+//
+// @param {String} eventType Type of the event.
+// @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
+// @param {module:engine/view/element~Element} viewElement
+// @returns {Boolean} Whether the change was handled.
+function handleRemoval( eventType, style, viewElement ) {
+	if ( style && ( eventType == 'changeAttribute' || eventType == 'removeAttribute' ) ) {
+		viewElement.removeClass( style.className );
+
+		return true;
+	}
+
+	return false;
+}
+
+// Handles converting addition of the attribute.
+// Returns `true` when handling was processed correctly and further conversion can be performed.
+//
+// @param {String} eventType Type of the event.
+// @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
+// @param {module:engine/view/element~Element} viewElement
+// @returns {Boolean} Whether the change was handled.
+function handleAddition( evenType, style, viewElement ) {
+	if ( style && ( evenType == 'addAttribute' || evenType == 'changeAttribute' ) ) {
+		viewElement.addClass( style.className );
+
+		return true;
+	}
+
+	return false;
 }

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

@@ -36,11 +36,11 @@ export default class ImageStyle extends Plugin {
 			this._createButton( style );
 		}
 
-		// If there is no default image toolbar configuration, add all image styles buttons.
-		const imageToolbarConfig = this.editor.config.get( 'image.toolbar' );
+		// Push buttons to default image toolbar if one exists.
+		const defaultImageToolbarConfig = this.editor.config.get( 'image.defaultToolbar' );
 
-		if ( !imageToolbarConfig ) {
-			this.editor.config.set( 'image.toolbar', styles.map( style => style.name ) );
+		if ( defaultImageToolbarConfig ) {
+			styles.forEach( style => defaultImageToolbarConfig.push( style.name ) );
 		}
 	}
 
@@ -52,23 +52,21 @@ export default class ImageStyle extends Plugin {
 	 */
 	_createButton( style ) {
 		const editor = this.editor;
-		const command = editor.commands.get( 'imagestyle' );
-		const t = editor.t;
+		const command = editor.commands.get( style.name );
 
 		editor.ui.componentFactory.add( style.name, ( locale ) => {
 			const view = new ButtonView( locale );
 
 			view.set( {
-				label: t( style.title ),
-				icon: style.icon
+				label: style.title,
+				icon: style.icon,
+				tooltip: true
 			} );
 
 			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
-			view.bind( 'isOn' ).to( command, 'value', ( commandValue ) => {
-				return commandValue == style.value;
-			} );
+			view.bind( 'isOn' ).to( command, 'value' );
 
-			this.listenTo( view, 'execute', () => editor.execute( 'imagestyle', { value: style.value } ) );
+			this.listenTo( view, 'execute', () => editor.execute( style.name ) );
 
 			return view;
 		} );

+ 23 - 32
packages/ckeditor5-image/src/imagestyle/imagestylecommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from 'ckeditor5-core/src/command/command';
-import { isImage, getStyleByValue } from './utils';
+import { isImage } from '../utils';
 
 /**
  * The image style command. It is used to apply different image styles.
@@ -17,29 +17,31 @@ import { isImage, getStyleByValue } from './utils';
  */
 export default class ImageStyleCommand extends Command {
 	/**
-	 * Creates instance of the command.
+	 * Creates instance of the image style command. Each command instance is handling one style.
 	 *
 	 * @param {module:core/editor/editor~Editor} editor Editor instance.
-	 * @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>} styles Allowed styles.
+	 * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} styles Style to apply by this command.
 	 */
-	constructor( editor, styles ) {
+	constructor( editor, style ) {
 		super( editor );
+
 		/**
-		 * The current style value.
+		 * The current command value - `true` if style handled by the command is applied on currently selected image,
+		 * `false` otherwise.
 		 *
 		 * @readonly
 		 * @observable
-		 * @member {String} #value
+		 * @member {Boolean} #value
 		 */
 		this.set( 'value', false );
 
 		/**
-		 * Allowed image styles used by this command.
+		 * Style handled by this command.
 		 *
 		 * @readonly
-		 * @member {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>} #styles
+		 * @member {module:image/imagestyle/imagestyleengine~ImageStyleFormat} #style
 		 */
-		this.styles = styles;
+		this.style = style;
 
 		// Update current value and refresh state each time something change in model document.
 		this.listenTo( editor.document, 'changesDone', () => {
@@ -57,18 +59,16 @@ export default class ImageStyleCommand extends Command {
 		const doc = this.editor.document;
 		const element = doc.selection.getSelectedElement();
 
-		if ( isImage( element ) ) {
-			if ( element.hasAttribute( 'imageStyle' ) ) {
-				const value = element.getAttribute( 'imageStyle' );
+		if ( !element ) {
+			this.value = false;
+
+			return;
+		}
 
-				// Check if value exists.
-				this.value = ( getStyleByValue( value, this.styles ) ? value : false );
-			} else {
-				// When there is no `style` attribute - set value to null.
-				this.value = null;
-			}
+		if ( this.style.value === null ) {
+			this.value = !element.hasAttribute( 'imageStyle' );
 		} else {
-			this.value = false;
+			this.value = ( element.getAttribute( 'imageStyle' ) == this.style.value );
 		}
 	}
 
@@ -86,21 +86,12 @@ export default class ImageStyleCommand extends Command {
 	 *
 	 * @protected
 	 * @param {Object} options
-	 * @param {String} options.value Value to apply. It must be one of the values from styles passed to {@link #constructor}.
 	 * @param {module:engine/model/batch~Batch} [options.batch] Batch to collect all the change steps. New batch will be
 	 * created if this option is not set.
 	 */
-	_doExecute( options ) {
-		const currentValue = this.value;
-		const newValue = options.value;
-
-		// Check if new value is valid.
-		if ( !getStyleByValue( newValue, this.styles ) ) {
-			return;
-		}
-
-		// Stop if same value is already applied.
-		if ( currentValue == newValue ) {
+	_doExecute( options = {} ) {
+		// Stop if style is already applied.
+		if ( this.value ) {
 			return;
 		}
 
@@ -112,7 +103,7 @@ export default class ImageStyleCommand extends Command {
 		doc.enqueueChanges( () => {
 			const batch = options.batch || doc.batch();
 
-			batch.setAttribute( imageElement, 'imageStyle', newValue );
+			batch.setAttribute( imageElement, 'imageStyle', this.style.value );
 		} );
 	}
 }

+ 24 - 15
packages/ckeditor5-image/src/imagestyle/imagestyleengine.js

@@ -10,9 +10,9 @@
 import Plugin from 'ckeditor5-core/src/plugin';
 import ImageStyleCommand from './imagestylecommand';
 import ImageEngine from '../imageengine';
-import { viewToModelImageStyle, modelToViewSetStyle } from './converters';
-import fullSizeIcon from 'ckeditor5-core/theme/icons/align-center.svg';
-import sideIcon from 'ckeditor5-core/theme/icons/align-right.svg';
+import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters';
+import fullSizeIcon from 'ckeditor5-core/theme/icons/object-center.svg';
+import sideIcon from 'ckeditor5-core/theme/icons/object-right.svg';
 
 /**
  * The image style engine plugin. Sets default configuration, creates converters and registers
@@ -33,6 +33,7 @@ export default class ImageStyleEngine extends Plugin {
 	 */
 	init() {
 		const editor = this.editor;
+		const t = editor.t;
 		const doc = editor.document;
 		const schema = doc.schema;
 		const data = editor.data;
@@ -41,10 +42,10 @@ export default class ImageStyleEngine extends Plugin {
 		// Define default configuration.
 		editor.config.define( 'image.styles', [
 			// This option is equal to situation when no style is applied.
-			{ name: 'imageStyleFull', title: 'Full size image', icon: fullSizeIcon, value: null },
+			{ name: 'imageStyleFull', title: t( 'Full size image' ), icon: fullSizeIcon, value: null },
 
 			// This represents side image.
-			{ name: 'imageStyleSide', title: 'Side image', icon: sideIcon, value: 'side', className: 'image-style-side' }
+			{ name: 'imageStyleSide', title: t( 'Side image' ), icon: sideIcon, value: 'side', className: 'image-style-side' }
 		] );
 
 		// Get configuration.
@@ -55,7 +56,7 @@ export default class ImageStyleEngine extends Plugin {
 		schema.allow( { name: 'image', attributes: 'imageStyle', inside: '$root' } );
 
 		// Converters for imageStyle attribute from model to view.
-		const modelToViewConverter = modelToViewSetStyle( styles );
+		const modelToViewConverter = modelToViewStyleAttribute( styles );
 		editing.modelToView.on( 'addAttribute:imageStyle:image', modelToViewConverter );
 		data.modelToView.on( 'addAttribute:imageStyle:image', modelToViewConverter );
 		editing.modelToView.on( 'changeAttribute:imageStyle:image', modelToViewConverter );
@@ -64,24 +65,32 @@ export default class ImageStyleEngine extends Plugin {
 		data.modelToView.on( 'removeAttribute:imageStyle:image', modelToViewConverter );
 
 		// Converter for figure element from view to model.
+		data.viewToModel.on( 'element:figure', viewToModelStyleAttribute( styles ), { priority: 'low' } );
+
+		// Register separate command for each style.
 		for ( let style of styles ) {
-			// Create converter only for non-null values.
-			if ( style.value !== null ) {
-				data.viewToModel.on( 'element:figure', viewToModelImageStyle( style ), { priority: 'low' } );
-			}
+			editor.commands.set( style.name, new ImageStyleCommand( editor, style ) );
 		}
-
-		// Register image style command.
-		editor.commands.set( 'imagestyle', new ImageStyleCommand( editor, styles ) );
 	}
 }
 
 /**
  * Image style format descriptor.
  *
+ *	import fullIcon from 'path/to/icon.svg`;
+ *
+ *	const imageStyleFormat = {
+ *		name: 'fullSizeImage',
+ *		value: 'full',
+ *		icon: fullIcon,
+ *		title: `Full size image`,
+ *		class: `image-full-size`
+ *	}
+ *
  * @typedef {Object} module:image/imagestyle/imagestyleengine~ImageStyleFormat
- * @property {String} name Name of the style, it will be used to store style's button under that name in editor's
- * {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
+ * @property {String} name Name of the style. It will be used to:
+ * * register {@link module:core/command/command~Command command} which will apply this style,
+ * * store style's button in editor's {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
  * @property {String} value Value used to store this style in model attribute.
  * When value is `null` style will be used as default one. Default style does not apply any CSS class to the view element.
  * @property {String} icon SVG icon representation to use when creating style's button.

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

@@ -1,36 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module image/imagestyle/utils
- */
-
-import ModelElement from 'ckeditor5-engine/src/model/element';
-
-/**
- * Checks if provided modelElement is an instance of {@link module:engine/model/element~Element Element} and its name
- * equals to `image`.
- *
- * @param {module:engine/model/element~Element} modelElement
- * @returns {Boolean}
- */
-export function isImage( modelElement ) {
-	return modelElement instanceof ModelElement && modelElement.name == 'image';
-}
-
-/**
- * Returns style with given `value` from array of styles.
- *
- * @param {String} value
- * @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat> } styles
- * @return {module:image/imagestyle/imagestyleengine~ImageStyleFormat|undefined}
- */
-export function getStyleByValue( value, styles ) {
-	for ( let style of styles ) {
-		if ( style.value === value ) {
-			return style;
-		}
-	}
-}

+ 14 - 3
packages/ckeditor5-image/src/imagetoolbar.js

@@ -16,6 +16,8 @@ import ImageBalloonPanel from './ui/imageballoonpanel';
  * Image toolbar class. Creates image toolbar placed inside balloon panel that is showed when image widget is selected.
  * Toolbar components are created using editor's {@link module:ui/componentfactory~ComponentFactory ComponentFactory}
  * based on {@link module:core/editor/editor~Editor#config configuration} stored under `image.toolbar`.
+ * Other plugins can add new components to the default toolbar configuration by pushing them to `image.defaultToolbar`
+ * configuration. Default configuration is used when `image.toolbar` config is not present.
  *
  * @extends module:core/plugin~Plugin.
  */
@@ -23,12 +25,21 @@ export default class ImageToolbar extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
+	constructor( editor ) {
+		super( editor );
+
+		editor.config.set( 'image.defaultToolbar', [] );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
 	afterInit() {
 		const editor = this.editor;
-		const toolbarConfig = editor.config.get( 'image.toolbar' );
+		const toolbarConfig = editor.config.get( 'image.toolbar' ) || editor.config.get( 'image.defaultToolbar' );
 
 		// Don't add the toolbar if there is no configuration.
-		if ( !toolbarConfig ) {
+		if ( !toolbarConfig.length ) {
 			return;
 		}
 
@@ -40,7 +51,7 @@ export default class ImageToolbar extends Plugin {
 		Template.extend( panel.template, {
 			attributes: {
 				class: [
-					'ck-toolbar__container',
+					'ck-toolbar__container'
 				]
 			}
 		} );

+ 12 - 0
packages/ckeditor5-image/src/utils.js

@@ -8,6 +8,7 @@
  */
 
 import { widgetize, isWidget } from './widget/utils';
+import ModelElement from 'ckeditor5-engine/src/model/element';
 
 const imageSymbol = Symbol( 'isImage' );
 
@@ -34,3 +35,14 @@ export function toImageWidget( viewElement ) {
 export function isImageWidget( viewElement ) {
 	return !!viewElement.getCustomProperty( imageSymbol ) && isWidget( viewElement );
 }
+
+/**
+ * Checks if provided modelElement is an instance of {@link module:engine/model/element~Element Element} and its name
+ * is `image`.
+ *
+ * @param {module:engine/model/element~Element} modelElement
+ * @returns {Boolean}
+ */
+export function isImage( modelElement ) {
+	return modelElement instanceof ModelElement && modelElement.name == 'image';
+}

+ 18 - 5
packages/ckeditor5-image/tests/imagestyle/imagestyle.js

@@ -4,6 +4,7 @@
  */
 
 import ClassicTestEditor from 'ckeditor5-core/tests/_utils/classictesteditor';
+import ImageToolbar from 'ckeditor5-image/src/imagetoolbar';
 import ImageStyle from 'ckeditor5-image/src/imagestyle/imagestyle';
 import ImageStyleEngine from 'ckeditor5-image/src/imagestyle/imagestyleengine';
 import ButtonView from 'ckeditor5-ui/src/button/buttonview';
@@ -45,10 +46,10 @@ describe( 'ImageStyle', () => {
 	} );
 
 	it( 'should register buttons for each style', () => {
-		const command = editor.commands.get( 'imagestyle' );
 		const spy = sinon.spy( editor, 'execute' );
 
 		for ( let style of styles ) {
+			const command = editor.commands.get( style.name );
 			const buttonView =  editor.ui.componentFactory.create( style.name );
 
 			expect( buttonView ).to.be.instanceOf( ButtonView );
@@ -61,16 +62,28 @@ describe( 'ImageStyle', () => {
 			expect( buttonView.isEnabled ).to.be.false;
 
 			buttonView.fire( 'execute' );
-			sinon.assert.calledWithExactly( editor.execute, 'imagestyle', { value: style.value } );
+			sinon.assert.calledWithExactly( editor.execute, style.name );
 
 			spy.reset();
 		}
 	} );
 
-	it( 'should add buttons to image toolbar if there is no default configuration', () => {
-		const toolbarConfig =  editor.config.get( 'image.toolbar' );
+	it( 'should not add buttons to default image toolbar if image toolbar is not present', () => {
+		expect( editor.config.get( 'image.defaultToolbar' ) ).to.be.undefined;
+	} );
+
+	it( 'should add buttons to default image toolbar if toolbar is present', () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor.create( editorElement, {
+			plugins: [ ImageStyle, ImageToolbar ]
+		} )
+			.then( newEditor => {
+				expect( newEditor.config.get( 'image.defaultToolbar' ) ).to.eql( [ 'imageStyleFull', 'imageStyleSide' ] );
 
-		expect( toolbarConfig ).to.eql( styles.map( style => style.name ) );
+				newEditor.destroy();
+			} );
 	} );
 
 	it( 'should not add buttons to image toolbar if configuration is present', () => {

+ 27 - 29
packages/ckeditor5-image/tests/imagestyle/imagestylecommand.js

@@ -8,18 +8,17 @@ import ImageStyleCommand from 'ckeditor5-image/src/imagestyle/imagestylecommand'
 import { setData, getData } from 'ckeditor5-engine/src/dev-utils/model';
 
 describe( 'ImageStyleCommand', () => {
-	const styles = [
-		{ name: 'defaultStyle', title: 'foo bar', icon: 'icon-1', value: null },
-		{ name: 'otherStyle', title: 'baz', icon: 'icon-2', value: 'other', className: 'other-class-name' }
-	];
+	const defaultStyle = { name: 'defaultStyle', title: 'foo bar', icon: 'icon-1', value: null };
+	const otherStyle = { name: 'otherStyle', title: 'baz', icon: 'icon-2', value: 'other', className: 'other-class-name' };
 
-	let document, command;
+	let document, defaultStyleCommand, otherStyleCommand;
 
 	beforeEach( () => {
 		return ModelTestEditor.create()
 			.then( newEditor => {
 				document = newEditor.document;
-				command = new ImageStyleCommand( newEditor, styles );
+				defaultStyleCommand = new ImageStyleCommand( newEditor, defaultStyle );
+				otherStyleCommand = new ImageStyleCommand( newEditor, otherStyle );
 
 				document.schema.registerItem( 'p', '$block' );
 
@@ -30,50 +29,46 @@ describe( 'ImageStyleCommand', () => {
 			} );
 	} );
 
-	it( 'should have false if image is not selected', () => {
+	it( 'command value should be false if no image is selected', () => {
 		setData( document, '[]<image></image>' );
 
-		expect( command.value ).to.be.false;
+		expect( defaultStyleCommand.value ).to.be.false;
+		expect( otherStyleCommand.value ).to.be.false;
 	} );
 
-	it( 'should have null if image without style is selected', () => {
+	it( 'should match default style if no imageStyle attribute is present', () => {
 		setData( document, '[<image></image>]' );
 
-		expect( command.value ).to.be.null;
+		expect( defaultStyleCommand.value ).to.be.true;
+		expect( otherStyleCommand.value ).to.be.false;
 	} );
 
-	it( 'should have proper value if image with style is selected', () => {
+	it( 'proper command should have true value when imageStyle attribute is present', () => {
 		setData( document, '[<image imageStyle="other"></image>]' );
 
-		expect( command.value ).to.equal( 'other' );
+		expect( defaultStyleCommand.value ).to.be.false;
+		expect( otherStyleCommand.value ).to.be.true;
 	} );
 
-	it( 'should return false if value is not allowed', () => {
+	it( 'should have false value if style does not match', () => {
 		setData( document, '[<image imageStyle="foo"></image>]' );
 
-		expect( command.value ).to.be.false;
+		expect( defaultStyleCommand.value ).to.be.false;
+		expect( otherStyleCommand.value ).to.be.false;
 	} );
 
 	it( 'should set proper value when executed', () => {
 		setData( document, '[<image></image>]' );
 
-		command._doExecute( { value: 'other' } );
+		otherStyleCommand._doExecute();
 
 		expect( getData( document ) ).to.equal( '[<image imageStyle="other"></image>]' );
 	} );
 
-	it( 'should do nothing when executed with wrong value', () => {
-		setData( document, '[<image></image>]' );
-
-		command._doExecute( { value: 'foo' } );
-
-		expect( getData( document ) ).to.equal( '[<image></image>]' );
-	} );
-
-	it( 'should do nothing when executed with same value', () => {
+	it( 'should do nothing when attribute already present', () => {
 		setData( document, '[<image imageStyle="other"></image>]' );
 
-		command._doExecute( { value: 'other' } );
+		otherStyleCommand._doExecute();
 
 		expect( getData( document ) ).to.equal( '[<image imageStyle="other"></image>]' );
 	} );
@@ -84,7 +79,7 @@ describe( 'ImageStyleCommand', () => {
 
 		setData( document, '[<image></image>]' );
 
-		command._doExecute( { value: 'other', batch } );
+		otherStyleCommand._doExecute( { batch } );
 
 		expect( getData( document ) ).to.equal( '[<image imageStyle="other"></image>]' );
 		sinon.assert.calledOnce( spy );
@@ -93,18 +88,21 @@ describe( 'ImageStyleCommand', () => {
 	it( 'should be enabled on image element', () => {
 		setData( document, '[<image></image>]' );
 
-		expect( command.isEnabled ).to.be.true;
+		expect( defaultStyleCommand.isEnabled ).to.be.true;
+		expect( otherStyleCommand.isEnabled ).to.be.true;
 	} );
 
 	it( 'should be disabled when not placed on image', () => {
 		setData( document, '[<p></p>]' );
 
-		expect( command.isEnabled ).to.be.false;
+		expect( defaultStyleCommand.isEnabled ).to.be.false;
+		expect( otherStyleCommand.isEnabled ).to.be.false;
 	} );
 
 	it( 'should be disabled when not placed directly on image', () => {
 		setData( document, '[<p></p><image></image>]' );
 
-		expect( command.isEnabled ).to.be.false;
+		expect( defaultStyleCommand.isEnabled ).to.be.false;
+		expect( otherStyleCommand.isEnabled ).to.be.false;
 	} );
 } );

+ 8 - 5
packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js

@@ -45,11 +45,14 @@ describe( 'ImageStyleEngine', () => {
 		expect( schema.check( { name: 'image', attributes: [ 'imageStyle', 'src' ], inside: '$root' } ) ).to.be.true;
 	} );
 
-	it( 'should register command', () => {
-		expect( editor.commands.has( 'imagestyle' ) ).to.be.true;
-		const command = editor.commands.get( 'imagestyle' );
-
-		expect( command ).to.be.instanceOf( ImageStyleCommand );
+	it( 'should register separate command for each style', () => {
+		expect( editor.commands.has( 'fullStyle' ) ).to.be.true;
+		expect( editor.commands.has( 'sideStyle' ) ).to.be.true;
+		expect( editor.commands.has( 'dummyStyle' ) ).to.be.true;
+
+		expect( editor.commands.get( 'fullStyle' ) ).to.be.instanceOf( ImageStyleCommand );
+		expect( editor.commands.get( 'sideStyle' ) ).to.be.instanceOf( ImageStyleCommand );
+		expect( editor.commands.get( 'dummyStyle' ) ).to.be.instanceOf( ImageStyleCommand );
 	} );
 
 	it( 'should convert from view to model', () => {

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

@@ -1,48 +0,0 @@
-/**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import { isImage, getStyleByValue } from 'ckeditor5-image/src/imagestyle/utils';
-import ModelElement from 'ckeditor5-engine/src/model/element';
-
-describe( 'ImageStyle utils', () => {
-	describe( 'isImage', () => {
-		it( 'should return true for image element', () => {
-			const image = new ModelElement( 'image' );
-
-			expect( isImage( image ) ).to.be.true;
-		} );
-
-		it( 'should return true false for different elements', () => {
-			const image = new ModelElement( 'foo' );
-
-			expect( isImage( image ) ).to.be.false;
-		} );
-
-		it( 'should return true false for null and undefined', () => {
-			expect( isImage( null ) ).to.be.false;
-			expect( isImage( undefined ) ).to.be.false;
-		} );
-	} );
-
-	describe( 'getStyleByValue', () => {
-		const styles = [
-			{ name: 'style 1', title: 'title 1', icon: 'style1-icon', value: 'style1', className: 'style1-class' },
-			{ name: 'style 2', title: 'title 2', icon: 'style2-icon', value: 'style2', className: 'style2-class' }
-		];
-
-		it( 'should return proper styles for values', () => {
-			expect( getStyleByValue( 'style1', styles ) ).to.equal( styles[ 0 ] );
-			expect( getStyleByValue( 'style2', styles ) ).to.equal( styles[ 1 ] );
-		} );
-
-		it( 'should return undefined when style is not found', () => {
-			expect( getStyleByValue( 'foo', styles ) ).to.be.undefined;
-		} );
-
-		it( 'should return undefined when empty styles array is provided', () => {
-			expect( getStyleByValue( 'style1', [] ) ).to.be.undefined;
-		} );
-	} );
-} );

+ 33 - 0
packages/ckeditor5-image/tests/imagetoolbar.js

@@ -43,6 +43,10 @@ describe( 'ImageToolbar', () => {
 		expect( editor.plugins.get( ImageToolbar ) ).to.be.instanceOf( ImageToolbar );
 	} );
 
+	it( 'should initialize image.defaultToolbar to an empty array', () => {
+		expect( editor.config.get( 'image.defaultToolbar' ) ).to.eql( [] );
+	} );
+
 	it( 'should not initialize if there is no configuration', () => {
 		const editorElement = global.document.createElement( 'div' );
 		global.document.body.appendChild( editorElement );
@@ -58,6 +62,25 @@ describe( 'ImageToolbar', () => {
 			} );
 	} );
 
+	it( 'should allow other plugins to alter default config', () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicEditor.create( editorElement, {
+			plugins: [ ImageToolbar, FakeButton, AlterDefaultConfig ]
+		} )
+			.then( newEditor => {
+				const panel = getBalloonPanelView( newEditor.ui.view.body );
+				const toolbar = panel.content.get( 0 );
+				const button = toolbar.items.get( 0 );
+
+				expect( newEditor.config.get( 'image.defaultToolbar' ) ).to.eql( [ 'fake_button' ] );
+				expect( button.label ).to.equal( 'fake button' );
+
+				newEditor.destroy();
+			} );
+	} );
+
 	it( 'should add BalloonPanelView to view body', () => {
 		expect( panel ).to.be.instanceOf( BalloonPanelView );
 	} );
@@ -164,4 +187,14 @@ describe( 'ImageToolbar', () => {
 			} );
 		}
 	}
+
+	class AlterDefaultConfig extends Plugin {
+		init() {
+			const defaultImageToolbarConfig = this.editor.config.get( 'image.defaultToolbar' );
+
+			if ( defaultImageToolbarConfig ) {
+				defaultImageToolbarConfig.push( 'fake_button' );
+			}
+		}
+	}
 } );

+ 21 - 1
packages/ckeditor5-image/tests/utils.js

@@ -4,7 +4,8 @@
  */
 
 import ViewElement from 'ckeditor5-engine/src/view/element';
-import { toImageWidget, isImageWidget } from 'ckeditor5-image/src/utils';
+import ModelElement from 'ckeditor5-engine/src/model/element';
+import { toImageWidget, isImageWidget, isImage } from 'ckeditor5-image/src/utils';
 import { isWidget } from 'ckeditor5-image/src/widget/utils';
 
 describe( 'image widget utils', () => {
@@ -30,4 +31,23 @@ describe( 'image widget utils', () => {
 			expect( isImageWidget( new ViewElement( 'p' ) ) ).to.be.false;
 		} );
 	} );
+
+	describe( 'isImage', () => {
+		it( 'should return true for image element', () => {
+			const image = new ModelElement( 'image' );
+
+			expect( isImage( image ) ).to.be.true;
+		} );
+
+		it( 'should return true false for different elements', () => {
+			const image = new ModelElement( 'foo' );
+
+			expect( isImage( image ) ).to.be.false;
+		} );
+
+		it( 'should return true false for null and undefined', () => {
+			expect( isImage( null ) ).to.be.false;
+			expect( isImage( undefined ) ).to.be.false;
+		} );
+	} );
 } );