Kaynağa Gözat

Merge pull request #140 from ckeditor/t/134

Feature: Allowed customization of default image styles. Defined formatting–oriented styles. Simplified `config.image.styles` syntax. Closes #134. Closes #135.

BREAKING CHANGE: From now on, the `imageStyleFull` uses `object-full-width.svg` icon.
Szymon Kupś 8 yıl önce
ebeveyn
işleme
35e5f0fda5

+ 4 - 25
packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js

@@ -7,42 +7,21 @@
 
 
 import './image-style-custom.scss';
 import './image-style-custom.scss';
 
 
-import fullSizeIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import alignLeftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
-import alignRightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
-
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style-custom' ), {
 	.create( document.querySelector( '#snippet-image-style-custom' ), {
 		image: {
 		image: {
 			styles: [
 			styles: [
 				// This option is equal to a situation where no style is applied.
 				// This option is equal to a situation where no style is applied.
-				{
-					name: 'imageStyleFull',
-					title: 'Full size image',
-					icon: fullSizeIcon,
-					value: null
-				},
+				'imageStyleFull',
 
 
 				// This represents an image aligned to left.
 				// This represents an image aligned to left.
-				{
-					name: 'imageStyleLeft',
-					title: 'Left aligned image',
-					icon: alignLeftIcon,
-					value: 'left',
-					className: 'image-style-left'
-				},
+				'imageStyleAlignLeft',
 
 
 				// This represents an image aligned to right.
 				// This represents an image aligned to right.
-				{
-					name: 'imageStyleRight',
-					title: 'Right aligned image',
-					icon: alignRightIcon,
-					value: 'right',
-					className: 'image-style-right'
-				}
+				'imageStyleAlignRight'
 			],
 			],
 
 
-			toolbar: [ 'imageTextAlternative', '|', 'imageStyleLeft', 'imageStyleFull', 'imageStyleRight' ]
+			toolbar: [ 'imageTextAlternative', '|', 'imageStyleAlignLeft', 'imageStyleFull', 'imageStyleAlignRight' ]
 		}
 		}
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {

+ 2 - 2
packages/ckeditor5-image/docs/_snippets/features/image-style-custom.scss

@@ -1,13 +1,13 @@
 // Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
 // Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 
 
-.image-style-left {
+.image-style-align-left {
     float: left;
     float: left;
     width: 50%;
     width: 50%;
     margin: 1em 1em 1em 0;
     margin: 1em 1em 1em 0;
 }
 }
 
 
-.image-style-right {
+.image-style-align-right {
     float: right;
     float: right;
     width: 50%;
     width: 50%;
     margin: 1em 0 1em 1em;
     margin: 1em 0 1em 1em;

+ 7 - 28
packages/ckeditor5-image/docs/features/image.md

@@ -111,45 +111,24 @@ Below you can see a demo of the editor with the image styles feature enabled. Th
 
 
 The available image styles can be configured using the {@link module:image/image~ImageConfig#styles `image.styles`} option.
 The available image styles can be configured using the {@link module:image/image~ImageConfig#styles `image.styles`} option.
 
 
-The following editor supports the default style plus left- and right-aligned images:
+The following editor supports the default full style plus left- and right-aligned images:
 
 
 ```js
 ```js
-import fullSizeIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import alignLeftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
-import alignRightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
-
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		image: {
 		image: {
 			// You need to configure the image toolbar too, so it uses the new style buttons.
 			// You need to configure the image toolbar too, so it uses the new style buttons.
-			toolbar: [ 'imageTextAlternative', '|', 'imageStyleLeft', 'imageStyleFull', 'imageStyleRight' ],
+			toolbar: [ 'imageTextAlternative', '|', 'imageStyleAlignLeft', 'imageStyleFull', 'imageStyleAlignRight' ],
 
 
 			styles: [
 			styles: [
 				// This option is equal to a situation where no style is applied.
 				// This option is equal to a situation where no style is applied.
-				{
-					name: 'imageStyleFull',
-					title: 'Full size image',
-					icon: fullSizeIcon,
-					value: null
-				},
+				'imageStyleFull',
 
 
 				// This represents an image aligned to left.
 				// This represents an image aligned to left.
-				{
-					name: 'imageStyleLeft',
-					title: 'Left aligned image',
-					icon: alignLeftIcon,
-					value: 'left',
-					className: 'image-style-left'
-				},
+				'imageStyleAlignLeft',
 
 
 				// This represents an image aligned to right.
 				// This represents an image aligned to right.
-				{
-					name: 'imageStyleRight',
-					title: 'Right aligned image',
-					icon: alignRightIcon,
-					value: 'right',
-					className: 'image-style-right'
-				}
+				'imageStyleAlignRight'
 			]
 			]
 		}
 		}
 	} )
 	} )
@@ -158,13 +137,13 @@ ClassicEditor
 ```
 ```
 
 
 ```css
 ```css
-.image-style-left {
+.image-style-align-left {
     float: left;
     float: left;
     width: 50%;
     width: 50%;
     margin: 1em 1em 1em 0;
     margin: 1em 1em 1em 0;
 }
 }
 
 
-.image-style-right {
+.image-style-align-right {
     float: right;
     float: right;
     width: 50%;
     width: 50%;
     margin: 1em 0 1em 1em;
     margin: 1em 0 1em 1em;

+ 41 - 6
packages/ckeditor5-image/src/imagestyle.js

@@ -37,7 +37,8 @@ export default class ImageStyle extends Plugin {
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	init() {
 	init() {
-		const styles = this.editor.config.get( 'image.styles' );
+		const editor = this.editor;
+		const styles = editor.plugins.get( ImageStyleEngine ).imageStyles;
 
 
 		for ( const style of styles ) {
 		for ( const style of styles ) {
 			this._createButton( style );
 			this._createButton( style );
@@ -79,6 +80,39 @@ export default class ImageStyle extends Plugin {
  *
  *
  * The default value is:
  * The default value is:
  *
  *
+ * 		const imageConfig = {
+ *			styles: [ 'imageStyleFull', 'imageStyleSide' ]
+ *		};
+ *
+ * which configures two default styles:
+ *
+ *  * 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.
+ *
+ * See {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles} to learn more about default
+ * styles provided by the image feature.
+ *
+ * The {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles default styles} can be customized,
+ * 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.
+ *
+ *		import customIcon from 'custom-icon.svg';
+ *
+ *		// ...
+ *
+ *		const imageConfig = {
+ *			styles: [
+ *				// This will only customize the icon of the "full" style.
+ *				// Note: 'right' is one of default icons provided by the feature.
+ *				{ name: 'imageStyleFull', icon: 'right' },
+ *
+ *				// This will customize the icon, title and CSS class of the default "side" style.
+ *				{ name: 'imageStyleSide', icon: customIcon, title: 'My side style', class: 'custom-side-image' }
+ *			]
+ *		};
+ *
+ * If none of the default styles is good enough, it is possible to define independent custom styles too:
+ *
  *		import fullSizeIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
  *		import fullSizeIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
  *		import sideIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
  *		import sideIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
  *
  *
@@ -86,15 +120,16 @@ export default class ImageStyle extends Plugin {
  *
  *
  *		const imageConfig = {
  *		const imageConfig = {
  *			styles: [
  *			styles: [
- *				// Option which defines a style which doesn't apply any class.
- *				// The style is titled "full" because such images are often styled to take 100% width of the content.
- *				{ name: 'imageStyleFull', title: t( 'Full size image' ), icon: fullSizeIcon, value: null },
+ *				// A completely custom full size style with no class, used as a default.
+ *				{ name: 'fullSize', title: 'Full size', icon: fullSizeIcon, isDefault: true },
  *
  *
- *				// Option which represents a side image.
- *				{ name: 'imageStyleSide', title: t( 'Side image' ), icon: sideIcon, value: 'side', className: 'image-style-side' }
+ *				{ name: 'side', title: 'To the side', icon: sideIcon, className: 'side-image' }
  *			]
  *			]
  *		};
  *		};
  *
  *
+ * Note: Setting `title` to one of {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine#localizedDefaultStylesTitles}
+ * will automatically translate it to the language of the editor.
+ *
  * Read more about styling images in the {@linkTODO Image styles guide}.
  * Read more about styling images in the {@linkTODO Image styles guide}.
  *
  *
  * The feature creates commands based on defined styles, so you can change the style of a selected image by executing
  * The feature creates commands based on defined styles, so you can change the style of a selected image by executing

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

@@ -26,8 +26,8 @@ export function modelToViewStyleAttribute( styles ) {
 		}
 		}
 
 
 		// Check if there is class name associated with given value.
 		// Check if there is class name associated with given value.
-		const newStyle = getStyleByValue( data.attributeNewValue, styles );
-		const oldStyle = getStyleByValue( data.attributeOldValue, styles );
+		const newStyle = getStyleByName( data.attributeNewValue, styles );
+		const oldStyle = getStyleByName( data.attributeOldValue, styles );
 		const viewElement = conversionApi.mapper.toViewElement( data.item );
 		const viewElement = conversionApi.mapper.toViewElement( data.item );
 
 
 		const isRemovalHandled = handleRemoval( eventType, oldStyle, viewElement );
 		const isRemovalHandled = handleRemoval( eventType, oldStyle, viewElement );
@@ -47,8 +47,8 @@ export function modelToViewStyleAttribute( styles ) {
  * @returns {Function} A view-to-model converter.
  * @returns {Function} A view-to-model converter.
  */
  */
 export function viewToModelStyleAttribute( styles ) {
 export function viewToModelStyleAttribute( styles ) {
-	// Convert only styles without `null` value.
-	const filteredStyles = styles.filter( style => style.value !== null );
+	// Convert only non–default styles.
+	const filteredStyles = styles.filter( style => !style.isDefault );
 
 
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
 		for ( const style of filteredStyles ) {
 		for ( const style of filteredStyles ) {
@@ -88,17 +88,17 @@ function viewToModelImageStyle( style, data, consumable, conversionApi ) {
 
 
 	// *** Step2: Convert to model.
 	// *** Step2: Convert to model.
 	consumable.consume( viewFigureElement, { class: style.className } );
 	consumable.consume( viewFigureElement, { class: style.className } );
-	modelImageElement.setAttribute( 'imageStyle', style.value );
+	modelImageElement.setAttribute( 'imageStyle', style.name );
 }
 }
 
 
-// Returns style with given `value` from array of styles.
+// Returns style with given `name` from array of styles.
 //
 //
-// @param {String} value
+// @param {String} name
 // @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat> } styles
 // @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat> } styles
 // @return {module:image/imagestyle/imagestyleengine~ImageStyleFormat|undefined}
 // @return {module:image/imagestyle/imagestyleengine~ImageStyleFormat|undefined}
-function getStyleByValue( value, styles ) {
+function getStyleByName( name, styles ) {
 	for ( const style of styles ) {
 	for ( const style of styles ) {
-		if ( style.value === value ) {
+		if ( style.name === name ) {
 			return style;
 			return style;
 		}
 		}
 	}
 	}

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

@@ -53,10 +53,10 @@ export default class ImageStyleCommand extends Command {
 
 
 		if ( !element ) {
 		if ( !element ) {
 			this.value = false;
 			this.value = false;
-		} else if ( this.style.value === null ) {
+		} else if ( this.style.isDefault ) {
 			this.value = !element.hasAttribute( 'imageStyle' );
 			this.value = !element.hasAttribute( 'imageStyle' );
 		} else {
 		} else {
-			this.value = ( element.getAttribute( 'imageStyle' ) == this.style.value );
+			this.value = ( element.getAttribute( 'imageStyle' ) == this.style.name );
 		}
 		}
 	}
 	}
 
 
@@ -79,7 +79,7 @@ export default class ImageStyleCommand extends Command {
 		doc.enqueueChanges( () => {
 		doc.enqueueChanges( () => {
 			const batch = options.batch || doc.batch();
 			const batch = options.batch || doc.batch();
 
 
-			batch.setAttribute( imageElement, 'imageStyle', this.style.value );
+			batch.setAttribute( imageElement, 'imageStyle', this.style.name );
 		} );
 		} );
 	}
 	}
 }
 }

+ 218 - 20
packages/ckeditor5-image/src/imagestyle/imagestyleengine.js

@@ -11,8 +11,12 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ImageStyleCommand from './imagestylecommand';
 import ImageStyleCommand from './imagestylecommand';
 import ImageEngine from '../image/imageengine';
 import ImageEngine from '../image/imageengine';
 import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters';
 import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters';
-import fullSizeIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import sideIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
+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
  * The image style engine plugin. It sets the default configuration, creates converters and registers
@@ -28,28 +32,28 @@ export default class ImageStyleEngine extends Plugin {
 		return [ ImageEngine ];
 		return [ ImageEngine ];
 	}
 	}
 
 
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ImageStyleEngine';
+	}
+
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
-		const t = editor.t;
 		const doc = editor.document;
 		const doc = editor.document;
 		const schema = doc.schema;
 		const schema = doc.schema;
 		const data = editor.data;
 		const data = editor.data;
 		const editing = editor.editing;
 		const editing = editor.editing;
 
 
 		// Define default configuration.
 		// Define default configuration.
-		editor.config.define( 'image.styles', [
-			// This option is equal to situation when no style is applied.
-			{ name: 'imageStyleFull', title: t( 'Full size image' ), icon: fullSizeIcon, value: null },
-
-			// This represents side image.
-			{ name: 'imageStyleSide', title: t( 'Side image' ), icon: sideIcon, value: 'side', className: 'image-style-side' }
-		] );
+		editor.config.define( 'image.styles', [ 'imageStyleFull', 'imageStyleSide' ] );
 
 
 		// Get configuration.
 		// Get configuration.
-		const styles = editor.config.get( 'image.styles' );
+		const styles = this.imageStyles;
 
 
 		// Allow imageStyle attribute in image.
 		// Allow imageStyle attribute in image.
 		// We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559.
 		// We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559.
@@ -72,28 +76,222 @@ export default class ImageStyleEngine extends Plugin {
 			editor.commands.add( style.name, new ImageStyleCommand( editor, style ) );
 			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 3 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.
  * Image style format descriptor.
  *
  *
- *		import fullIcon from 'path/to/icon.svg`;
+ *		import fullWidthIcon from 'path/to/icon.svg`;
  *
  *
  *		const imageStyleFormat = {
  *		const imageStyleFormat = {
  *			name: 'fullSizeImage',
  *			name: 'fullSizeImage',
- *			value: 'full',
- *			icon: fullIcon,
+ *			icon: fullWidthIcon,
  *			title: 'Full size image',
  *			title: 'Full size image',
- *			class: 'image-full-size'
+ *			className: 'image-full-size'
  *		}
  *		}
  *
  *
  * @typedef {Object} module:image/imagestyle/imagestyleengine~ImageStyleFormat
  * @typedef {Object} module:image/imagestyle/imagestyleengine~ImageStyleFormat
- * @property {String} name The name of the style. It will be used to:
+ * @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,
  * * 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}.
- * @property {String} value A value used to store this style in the model attribute.
- * When the value is `null`, 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 An SVG icon source (as XML string) to use when creating the style's button.
+ * * 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} title The style's title.
  * @property {String} className The CSS class used to represent the style in view.
  * @property {String} className The CSS class used to represent the style in view.
  */
  */

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

@@ -12,9 +12,9 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 describe( 'ImageStyle', () => {
 describe( 'ImageStyle', () => {
 	let editor;
 	let editor;
 	const styles = [
 	const styles = [
-		{ name: 'style 1', title: 'Style 1 title', icon: 'style1-icon', value: null },
-		{ name: 'style 2', title: 'Style 2 title', icon: 'style2-icon', value: 'style2', cssClass: 'style2-class' },
-		{ name: 'style 3', title: 'Style 3 title', icon: 'style3-icon', value: 'style3', cssClass: 'style3-class' }
+		{ 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( () => {

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

@@ -8,8 +8,8 @@ import ImageStyleCommand from '../../src/imagestyle/imagestylecommand';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 describe( 'ImageStyleCommand', () => {
 describe( 'ImageStyleCommand', () => {
-	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' };
+	const defaultStyle = { name: 'defaultStyle', title: 'foo bar', icon: 'icon-1', isDefault: true };
+	const otherStyle = { name: 'otherStyle', title: 'baz', icon: 'icon-2', className: 'other-class-name' };
 
 
 	let document, defaultStyleCommand, otherStyleCommand;
 	let document, defaultStyleCommand, otherStyleCommand;
 
 
@@ -44,7 +44,7 @@ describe( 'ImageStyleCommand', () => {
 	} );
 	} );
 
 
 	it( 'proper command should have true value when imageStyle attribute is present', () => {
 	it( 'proper command should have true value when imageStyle attribute is present', () => {
-		setData( document, '[<image imageStyle="other"></image>]' );
+		setData( document, '[<image imageStyle="otherStyle"></image>]' );
 
 
 		expect( defaultStyleCommand.value ).to.be.false;
 		expect( defaultStyleCommand.value ).to.be.false;
 		expect( otherStyleCommand.value ).to.be.true;
 		expect( otherStyleCommand.value ).to.be.true;
@@ -62,15 +62,15 @@ describe( 'ImageStyleCommand', () => {
 
 
 		otherStyleCommand.execute();
 		otherStyleCommand.execute();
 
 
-		expect( getData( document ) ).to.equal( '[<image imageStyle="other"></image>]' );
+		expect( getData( document ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
 	} );
 	} );
 
 
 	it( 'should do nothing when attribute already present', () => {
 	it( 'should do nothing when attribute already present', () => {
-		setData( document, '[<image imageStyle="other"></image>]' );
+		setData( document, '[<image imageStyle="otherStyle"></image>]' );
 
 
 		otherStyleCommand.execute();
 		otherStyleCommand.execute();
 
 
-		expect( getData( document ) ).to.equal( '[<image imageStyle="other"></image>]' );
+		expect( getData( document ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
 	} );
 	} );
 
 
 	it( 'should allow to provide batch instance', () => {
 	it( 'should allow to provide batch instance', () => {
@@ -81,7 +81,7 @@ describe( 'ImageStyleCommand', () => {
 
 
 		otherStyleCommand.execute( { batch } );
 		otherStyleCommand.execute( { batch } );
 
 
-		expect( getData( document ) ).to.equal( '[<image imageStyle="other"></image>]' );
+		expect( getData( document ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
 		sinon.assert.calledOnce( spy );
 		sinon.assert.calledOnce( spy );
 	} );
 	} );
 
 

+ 439 - 171
packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js

@@ -7,241 +7,509 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import ImageStyleEngine from '../../src/imagestyle/imagestyleengine';
 import ImageStyleEngine from '../../src/imagestyle/imagestyleengine';
 import ImageEngine from '../../src/image/imageengine';
 import ImageEngine from '../../src/image/imageengine';
 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';
+
 describe( 'ImageStyleEngine', () => {
 describe( 'ImageStyleEngine', () => {
-	let editor, document, viewDocument;
-
-	beforeEach( () => {
-		return VirtualTestEditor
-			.create( {
-				plugins: [ ImageStyleEngine ],
-				image: {
-					styles: [
-						{ name: 'fullStyle', title: 'foo', icon: 'object-center', value: null },
-						{ name: 'sideStyle', title: 'bar', icon: 'object-right', value: 'side', className: 'side-class' },
-						{ name: 'dummyStyle', title: 'baz', icon: 'object-dummy', value: 'dummy', className: 'dummy-class' }
-					]
-				}
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-				document = editor.document;
-				viewDocument = editor.editing.view;
-			} );
-	} );
+	let editor, plugin, document, viewDocument;
 
 
-	it( 'should be loaded', () => {
-		expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine );
+	afterEach( () => {
+		editor.destroy();
 	} );
 	} );
 
 
-	it( 'should load image engine', () => {
-		expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine );
-	} );
+	describe( 'plugin', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEngine ],
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+				} );
+		} );
 
 
-	it( 'should set schema rules for image style', () => {
-		const schema = document.schema;
+		it( 'should be loaded', () => {
+			expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine );
+		} );
 
 
-		expect( schema.check( { name: 'image', attributes: [ 'imageStyle', 'src' ], inside: '$root' } ) ).to.be.true;
+		it( 'should load image engine', () => {
+			expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine );
+		} );
 	} );
 	} );
 
 
-	it( 'should register separate command for each style', () => {
-		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 );
-	} );
+	describe( 'init', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEngine ],
+					image: {
+						styles: [
+							{ name: 'fullStyle', title: 'foo', icon: 'object-center', isDefault: true },
+							{ name: 'sideStyle', title: 'bar', icon: 'object-right', className: 'side-class' },
+							{ name: 'dummyStyle', title: 'baz', icon: 'object-dummy', className: 'dummy-class' },
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					document = editor.document;
+					viewDocument = editor.editing.view;
+				} );
+		} );
 
 
-	it( 'should convert from view to model', () => {
-		editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
+		it( 'should define image.styles config', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEngine ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
 
 
-		expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image imageStyle="side" src="foo.png"></image>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget side-class" contenteditable="false">' +
-				'<img src="foo.png"></img>' +
-			'</figure>' );
-	} );
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'imageStyleFull', 'imageStyleSide' ] );
+				} );
+		} );
 
 
-	it( 'should not convert from view to model if class is not defined', () => {
-		editor.setData( '<figure class="image foo-bar"><img src="foo.png" /></figure>' );
+		it( 'should set schema rules for image style', () => {
+			const schema = document.schema;
 
 
-		expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+			expect( schema.check( { name: 'image', attributes: [ 'imageStyle', 'src' ], inside: '$root' } ) ).to.be.true;
+		} );
+
+		it( 'should register separate command for each style', () => {
+			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 not convert from view to model when not in image figure', () => {
-		editor.setData( '<figure class="side-class"></figure>' );
+		it( 'should convert from view to model', () => {
+			editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
 
 
-		expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal( '' );
-	} );
+			expect( getModelData( document, { withoutSelection: true } ) )
+				.to.equal( '<image imageStyle="sideStyle" src="foo.png"></image>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget side-class" contenteditable="false">' +
+					'<img src="foo.png"></img>' +
+				'</figure>' );
+		} );
 
 
-	it( 'should not convert from view to model if schema prevents it', () => {
-		document.schema.disallow( { name: 'image', attributes: 'imageStyle' } );
-		editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
+		it( 'should not convert from view to model if class is not defined', () => {
+			editor.setData( '<figure class="image foo-bar"><img src="foo.png" /></figure>' );
 
 
-		expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
 
 
-	it( 'should convert model to view: adding attribute', () => {
-		setModelData( document, '<image src="foo.png"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+		it( 'should not convert from view to model when not in image figure', () => {
+			editor.setData( '<figure class="side-class"></figure>' );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', 'side' );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal( '' );
 		} );
 		} );
 
 
-		expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+		it( 'should not convert from view to model if schema prevents it', () => {
+			document.schema.disallow( { name: 'image', attributes: 'imageStyle' } );
+			editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
+
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
 
 
-	it( 'should convert model to view: removing attribute', () => {
-		setModelData( document, '<image src="foo.png" imageStyle="side"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+		it( 'should convert model to view: adding attribute', () => {
+			setModelData( document, '<image src="foo.png"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
+			} );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', null );
+			expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
 		} );
 		} );
 
 
-		expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+		it( 'should convert model to view: removing attribute', () => {
+			setModelData( document, '<image src="foo.png" imageStyle="imageStyleSide"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
 
 
-	it( 'should convert model to view: change attribute', () => {
-		setModelData( document, '<image src="foo.png" imageStyle="dummy"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', null );
+			} );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', 'side' );
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
 		} );
 		} );
 
 
-		expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
+		it( 'should convert model to view: change attribute', () => {
+			setModelData( document, '<image src="foo.png" imageStyle="dummy"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
 
 
-		// https://github.com/ckeditor/ckeditor5-image/issues/132
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
+			// https://github.com/ckeditor/ckeditor5-image/issues/132
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', 'dummy' );
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', 'dummyStyle' );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image dummy-class"><img src="foo.png"></figure>' );
+
+			// https://github.com/ckeditor/ckeditor5-image/issues/132
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget dummy-class" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
 		} );
 		} );
 
 
-		expect( editor.getData() ).to.equal( '<figure class="image dummy-class"><img src="foo.png"></figure>' );
+		it( 'should not convert from model to view if already consumed: adding attribute', () => {
+			editor.editing.modelToView.on( 'addAttribute:imageStyle', ( evt, data, consumable ) => {
+				consumable.consume( data.item, 'addAttribute:imageStyle' );
+			}, { priority: 'high' } );
 
 
-		// https://github.com/ckeditor/ckeditor5-image/issues/132
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget dummy-class" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+			setModelData( document, '<image src="foo.png"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
+			} );
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should not convert from model to view if already consumed: removing attribute', () => {
+			editor.editing.modelToView.on( 'removeAttribute:imageStyle', ( evt, data, consumable ) => {
+				consumable.consume( data.item, 'removeAttribute:imageStyle' );
+			}, { priority: 'high' } );
 
 
-	it( 'should not convert from model to view if already consumed: adding attribute', () => {
-		editor.editing.modelToView.on( 'addAttribute:imageStyle', ( evt, data, consumable ) => {
-			consumable.consume( data.item, 'addAttribute:imageStyle' );
-		}, { priority: 'high' } );
+			setModelData( document, '<image src="foo.png" imageStyle="sideStyle"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
 
 
-		setModelData( document, '<image src="foo.png"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', null );
+			} );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', 'side' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
 		} );
 		} );
 
 
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+		it( 'should not convert from model to view if already consumed: change attribute', () => {
+			editor.editing.modelToView.on( 'changeAttribute:imageStyle', ( evt, data, consumable ) => {
+				consumable.consume( data.item, 'changeAttribute:imageStyle' );
+			}, { priority: 'high' } );
 
 
-	it( 'should not convert from model to view if already consumed: removing attribute', () => {
-		editor.editing.modelToView.on( 'removeAttribute:imageStyle', ( evt, data, consumable ) => {
-			consumable.consume( data.item, 'removeAttribute:imageStyle' );
-		}, { priority: 'high' } );
+			setModelData( document, '<image src="foo.png" imageStyle="dummyStyle"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
 
 
-		setModelData( document, '<image src="foo.png" imageStyle="side"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
+			} );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', null );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget dummy-class" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
 		} );
 		} );
 
 
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+		it( 'should not convert from model to view if style is not present: adding attribute', () => {
+			setModelData( document, '<image src="foo.png"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', 'foo' );
+			} );
 
 
-	it( 'should not convert from model to view if already consumed: change attribute', () => {
-		editor.editing.modelToView.on( 'changeAttribute:imageStyle', ( evt, data, consumable ) => {
-			consumable.consume( data.item, 'changeAttribute:imageStyle' );
-		}, { priority: 'high' } );
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
 
 
-		setModelData( document, '<image src="foo.png" imageStyle="dummy"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+		it( 'should not convert from model to view if style is not present: change attribute', () => {
+			setModelData( document, '<image src="foo.png" imageStyle="dummy"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', 'side' );
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', 'foo' );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
 		} );
 		} );
 
 
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget dummy-class" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
+		it( 'should not convert from model to view if style is not present: remove attribute', () => {
+			setModelData( document, '<image src="foo.png" imageStyle="foo"></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'imageStyle', null );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
 	} );
 	} );
 
 
-	it( 'should not convert from model to view if style is not present: adding attribute', () => {
-		setModelData( document, '<image src="foo.png"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+	describe( 'imageStyles()', () => {
+		it( 'should fall back to defaults when no image.styles', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEngine ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'imageStyleFull', 'imageStyleSide' ] );
+				} );
+		} );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', 'foo' );
+		it( 'should not alter the image.styles config', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEngine ],
+					image: {
+						styles: [
+							'imageStyleSide'
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'imageStyleSide' ] );
+				} );
 		} );
 		} );
 
 
-		expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
-	} );
+		it( 'should not alter object definitions in the image.styles config', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEngine ],
+					image: {
+						styles: [
+							{ name: 'imageStyleSide' }
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ { name: 'imageStyleSide' } ] );
+				} );
+		} );
 
 
-	it( 'should not convert from model to view if style is not present: change attribute', () => {
-		setModelData( document, '<image src="foo.png" imageStyle="dummy"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+		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 );
+				} );
+		} );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', 'foo' );
+		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
+				} );
+			} );
 		} );
 		} );
 
 
-		expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
+		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', () => {
+				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' }
+						);
+					} );
+			} );
+		} );
 	} );
 	} );
 
 
-	it( 'should not convert from model to view if style is not present: remove attribute', () => {
-		setModelData( document, '<image src="foo.png" imageStyle="foo"></image>' );
-		const image = document.getRoot().getChild( 0 );
-		const batch = document.batch();
+	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'
+					} );
+				} );
+		} );
+	} );
 
 
-		document.enqueueChanges( () => {
-			batch.setAttribute( image, 'imageStyle', null );
+	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'
+				}
+			} );
 		} );
 		} );
+	} );
 
 
-		expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-		expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-			'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-		);
+	describe( 'defaultIcons', () => {
+		it( 'should be defined', () => {
+			expect( ImageStyleEngine.defaultIcons ).to.deep.equal( {
+				full: fullWidthIcon,
+				left: leftIcon,
+				right: rightIcon,
+				center: centerIcon,
+			} );
+		} );
 	} );
 	} );
 } );
 } );

+ 11 - 1
packages/ckeditor5-image/tests/manual/imagestyle.html

@@ -1,10 +1,20 @@
-<div id="editor">
+<h2>Semantic–oriented images</h2>
+
+<div id="editor-semantic">
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<figure class="image">
 	<figure class="image">
 		<img src="sample.jpg" alt="" />
 		<img src="sample.jpg" alt="" />
 	</figure>
 	</figure>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
+</div>
+
+<h2>Formatting–oriented images</h2>
+<div id="editor-formatting">
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
+	<figure class="image image-style-align-center">
+		<img src="sample.jpg" alt="" />
+	</figure>
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
 </div>
 </div>

+ 28 - 2
packages/ckeditor5-image/tests/manual/imagestyle.js

@@ -17,7 +17,7 @@ import ImageStyle from '../../src/imagestyle';
 import ImageToolbar from '../../src/imagetoolbar';
 import ImageToolbar from '../../src/imagetoolbar';
 
 
 ClassicEditor
 ClassicEditor
-	.create( document.querySelector( '#editor' ), {
+	.create( document.querySelector( '#editor-semantic' ), {
 		plugins: [
 		plugins: [
 			ImageToolbar,
 			ImageToolbar,
 			EnterPlugin,
 			EnterPlugin,
@@ -35,7 +35,33 @@ ClassicEditor
 		}
 		}
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
-		window.editor = editor;
+		window.editorSemantic = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+ClassicEditor
+	.create( document.querySelector( '#editor-formatting' ), {
+		plugins: [
+			ImageToolbar,
+			EnterPlugin,
+			TypingPlugin,
+			ParagraphPlugin,
+			HeadingPlugin,
+			ImagePlugin,
+			UndoPlugin,
+			ClipboardPlugin,
+			ImageStyle
+		],
+		toolbar: [ 'headings', 'undo', 'redo' ],
+		image: {
+			styles: [ 'imageStyleAlignLeft', 'imageStyleAlignCenter', 'imageStyleAlignRight' ],
+			toolbar: [ 'imageStyleAlignLeft', 'imageStyleAlignCenter', 'imageStyleAlignRight' ]
+		}
+	} )
+	.then( editor => {
+		window.editorFormatting = editor;
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

+ 7 - 0
packages/ckeditor5-image/tests/manual/imagestyle.md

@@ -1,6 +1,13 @@
 ## ImageStyle feature
 ## ImageStyle feature
 
 
+### The "semantic–oriented" editor
+
 * Click on image - toolbar with icons should appear. "Full size image" icon should be selected.
 * Click on image - toolbar with icons should appear. "Full size image" icon should be selected.
 * Click on "Side image" icon. Image should be aligned to right.
 * Click on "Side image" icon. Image should be aligned to right.
 * Click on "Full size image" icon. Image should be back to its original state.
 * Click on "Full size image" icon. Image should be back to its original state.
 * Resize the browser window so the scrollbar is visible. Click on image and scroll editor contents - check if toolbar is placed in proper position.
 * Resize the browser window so the scrollbar is visible. Click on image and scroll editor contents - check if toolbar is placed in proper position.
+
+### The "formatting–oriented" editor
+
+* Click on image - toolbar with icons should appear. "Centered image" icon should be selected.
+* Check if other icons icons change the alignment of the image.

+ 28 - 2
packages/ckeditor5-image/theme/theme.scss

@@ -1,15 +1,41 @@
 // Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
 // Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 
 
+@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_spacing';
+
+$ck-image-spacing: 5 * ck-spacing( 'small' );
+$ck-image-max-width: 50%;
+
 .ck-editor__editable {
 .ck-editor__editable {
 	.image {
 	.image {
 		text-align: center;
 		text-align: center;
 		clear: both;
 		clear: both;
 
 
+		&.image-style-side,
+		&.image-style-align-left,
+		&.image-style-align-center,
+		&.image-style-align-right {
+			max-width: $ck-image-max-width;
+		}
+
 		&.image-style-side {
 		&.image-style-side {
 			float: right;
 			float: right;
-			margin-left: 0.8em;
-			max-width: 50%;
+			margin-left: $ck-image-spacing;
+		}
+
+		&.image-style-align-left {
+			float: left;
+			margin-right: $ck-image-spacing;
+		}
+
+		&.image-style-align-center {
+			margin-left: auto;
+			margin-right: auto;
+		}
+
+		&.image-style-align-right {
+			float: right;
+			margin-left: $ck-image-spacing;
 		}
 		}
 	}
 	}