Explorar el Código

Merge pull request #298 from ckeditor/t/ckeditor5-image/241

Feature: Introduced image widget resizer. Closes #241.
Piotrek Koszuliński hace 6 años
padre
commit
5cf539406c
Se han modificado 27 ficheros con 1464 adiciones y 35 borrados
  1. 0 0
      packages/ckeditor5-image/docs/_snippets/features/build-image-source.html
  2. 14 0
      packages/ckeditor5-image/docs/_snippets/features/build-image-source.js
  3. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-caption.js
  4. 14 0
      packages/ckeditor5-image/docs/_snippets/features/image-resize-px.html
  5. 25 0
      packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js
  6. 14 0
      packages/ckeditor5-image/docs/_snippets/features/image-resize.html
  7. 22 0
      packages/ckeditor5-image/docs/_snippets/features/image-resize.js
  8. 1 0
      packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js
  9. 1 0
      packages/ckeditor5-image/docs/_snippets/features/image-style.js
  10. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image-toolbar.js
  11. 1 1
      packages/ckeditor5-image/docs/_snippets/features/image.js
  12. 28 2
      packages/ckeditor5-image/docs/features/image.md
  13. 5 1
      packages/ckeditor5-image/package.json
  14. 165 0
      packages/ckeditor5-image/src/imageresize.js
  15. 58 0
      packages/ckeditor5-image/src/imageresize/imageresizecommand.js
  16. 686 0
      packages/ckeditor5-image/tests/imageresize.js
  17. 88 0
      packages/ckeditor5-image/tests/imageresize/imageresizecommand.js
  18. 79 0
      packages/ckeditor5-image/tests/manual/imageresize.html
  19. 63 0
      packages/ckeditor5-image/tests/manual/imageresize.js
  20. 10 0
      packages/ckeditor5-image/tests/manual/imageresize.md
  21. 83 0
      packages/ckeditor5-image/tests/manual/imageresizepx.html
  22. 64 0
      packages/ckeditor5-image/tests/manual/imageresizepx.js
  23. 10 0
      packages/ckeditor5-image/tests/manual/imageresizepx.md
  24. 3 27
      packages/ckeditor5-image/tests/manual/imagestyle.js
  25. 25 0
      packages/ckeditor5-image/theme/imageresize.css
  26. 3 1
      packages/ckeditor5-image/theme/imagestyle.css
  27. 0 1
      packages/ckeditor5-image/theme/imageuploadprogress.css

+ 0 - 0
packages/ckeditor5-image/docs/_snippets/features/build-image-source.html


+ 14 - 0
packages/ckeditor5-image/docs/_snippets/features/build-image-source.js

@@ -0,0 +1,14 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+
+import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize';
+
+ClassicEditor.builtinPlugins.push( ImageResize );
+
+window.ClassicEditor = ClassicEditor;

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-caption.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-caption' ), {
-		removePlugins: [ 'ImageStyle' ],
+		removePlugins: [ 'ImageStyle', 'ImageResize' ],
 		image: {
 			toolbar: [ 'imageTextAlternative' ]
 		},

+ 14 - 0
packages/ckeditor5-image/docs/_snippets/features/image-resize-px.html

@@ -0,0 +1,14 @@
+<div id="snippet-image-resize-px">
+	<h2>Resize me!</h2>
+
+	<figure class="image">
+		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
+		<figcaption>Autumn fields by Aleksander Nowodziński</figcaption>
+	</figure>
+
+	<h2>Resized image (width: 400px):</h2>
+
+	<figure class="image image_resized" style="width:400px;">
+		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
+	</figure>
+</div>

+ 25 - 0
packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js

@@ -0,0 +1,25 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config.js';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-image-resize-px' ), {
+		image: {
+			resizeUnit: 'px'
+		},
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		cloudServices: CS_CONFIG
+	} )
+	.then( editor => {
+		window.editorResizePX = editor;
+	} )
+	.catch( err => {
+		console.error( err );
+	} );

+ 14 - 0
packages/ckeditor5-image/docs/_snippets/features/image-resize.html

@@ -0,0 +1,14 @@
+<div id="snippet-image-resize">
+	<h2>Resize me!</h2>
+
+	<figure class="image">
+		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
+		<figcaption>Autumn fields by Aleksander Nowodziński</figcaption>
+	</figure>
+
+	<h2>Resized image (width: 75%):</h2>
+
+	<figure class="image image_resized" style="width:75%;">
+		<img src="%BASE_PATH%/assets/img/fields.jpg" alt="Autumn fields">
+	</figure>
+</div>

+ 22 - 0
packages/ckeditor5-image/docs/_snippets/features/image-resize.js

@@ -0,0 +1,22 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config.js';
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-image-resize' ), {
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		cloudServices: CS_CONFIG
+	} )
+	.then( editor => {
+		window.editorResize = editor;
+	} )
+	.catch( err => {
+		console.error( err );
+	} );

+ 1 - 0
packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js

@@ -9,6 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style-custom' ), {
+		removePlugins: [ 'ImageResize' ],
 		image: {
 			styles: [
 				// This option is equal to a situation where no style is applied.

+ 1 - 0
packages/ckeditor5-image/docs/_snippets/features/image-style.js

@@ -9,6 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style' ), {
+		removePlugins: [ 'ImageResize' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image-toolbar.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-toolbar' ), {
-		removePlugins: [ 'ImageCaption', 'ImageStyle' ],
+		removePlugins: [ 'ImageCaption', 'ImageStyle', 'ImageResize' ],
 		image: {
 			toolbar: [ 'imageTextAlternative' ]
 		},

+ 1 - 1
packages/ckeditor5-image/docs/_snippets/features/image.js

@@ -9,7 +9,7 @@ import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image' ), {
-		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle' ],
+		removePlugins: [ 'ImageToolbar', 'ImageCaption', 'ImageStyle', 'ImageResize' ],
 		toolbar: {
 			viewportTopOffset: window.getViewportTopOffsetConfig()
 		},

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

@@ -3,7 +3,7 @@ title: Images
 category: features
 ---
 
-{@snippet build-classic-source}
+{@snippet features/build-image-source}
 
 The [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckeditor5-image) package contains multiple plugins that implement various image-related features:
 
@@ -13,9 +13,12 @@ The [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckedit
 * {@link module:image/imagestyle~ImageStyle} adds support for image styles.
 * {@link module:image/imagetextalternative~ImageTextAlternative} adds support for adding text alternative.
 * {@link module:image/imageupload~ImageUpload} adds support for uploading dropped or pasted images (see: {@link features/image-upload Image upload}).
+* {@link module:image/imageresize~ImageResize} adds support for resizing images.
 
 <info-box info>
-	All features listed above are enabled by default in all builds.
+	All features listed above expect image resize are enabled by default in all builds.
+
+	Check the documentation of each sub-feature to learn more about it.
 </info-box>
 
 ## Base image support
@@ -175,6 +178,29 @@ See the {@link features/image-upload Image upload} guide.
 
 Responsive images support in CKEditor 5 is brought by the {@link features/easy-image Easy Image} feature without any additional configuration. Learn more how to use the feature in your project in the {@link features/easy-image#responsive-images "Easy Image integration"} guide.
 
+## Resizing images
+
+TODO:
+
+* overview
+* markup
+* styling concerns
+* installation
+* note about it not being enabled by default
+* a note about followups? a dialog, a toggle predefined sizes (e.g. 40%, 50%, 75%, 100%)
+
+{@snippet features/image-resize}
+
+### Using pixels instead of percentage width
+
+TODO:
+
+* overview
+* downsides
+* configuration
+
+{@snippet features/image-resize-px}
+
 ## Installation
 
 To add image features to your editor, install the [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckeditor5-image) package:

+ 5 - 1
packages/ckeditor5-image/package.json

@@ -19,13 +19,17 @@
   },
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^11.1.3",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.2",
     "@ckeditor/ckeditor5-clipboard": "^12.0.1",
     "@ckeditor/ckeditor5-editor-classic": "^12.1.3",
-    "@ckeditor/ckeditor5-enter": "^11.0.4",
+	"@ckeditor/ckeditor5-enter": "^11.0.4",
+	"@ckeditor/ckeditor5-easy-image": "^11.0.4",
     "@ckeditor/ckeditor5-heading": "^11.0.4",
+    "@ckeditor/ckeditor5-indent": "^10.0.1",
     "@ckeditor/ckeditor5-link": "^11.1.1",
     "@ckeditor/ckeditor5-list": "^12.0.4",
     "@ckeditor/ckeditor5-paragraph": "^11.0.4",
+    "@ckeditor/ckeditor5-table": "^13.0.0",
     "@ckeditor/ckeditor5-typing": "^12.1.1",
     "@ckeditor/ckeditor5-undo": "^11.0.4",
     "eslint": "^5.5.0",

+ 165 - 0
packages/ckeditor5-image/src/imageresize.js

@@ -0,0 +1,165 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module image/imageresize
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import WidgetResize from '@ckeditor/ckeditor5-widget/src/widgetresize';
+import ImageResizeCommand from './imageresize/imageresizecommand';
+
+import '../theme/imageresize.css';
+
+/**
+ * The image resize plugin.
+ *
+ * It adds a possibility to resize each image using handles.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageResize extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ WidgetResize ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ImageResize';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const command = new ImageResizeCommand( editor );
+
+		this._registerSchema();
+		this._registerConverters();
+
+		editor.commands.add( 'imageResize', command );
+
+		editor.editing.downcastDispatcher.on( 'insert:image', ( evt, data, conversionApi ) => {
+			const widget = conversionApi.mapper.toViewElement( data.item );
+
+			const resizer = editor.plugins
+				.get( WidgetResize )
+				.attachTo( {
+					unit: editor.config.get( 'image.resizeUnit' ) || '%',
+
+					modelElement: data.item,
+					viewElement: widget,
+					downcastWriter: conversionApi.writer,
+
+					getHandleHost( domWidgetElement ) {
+						return domWidgetElement.querySelector( 'img' );
+					},
+					getResizeHost( domWidgetElement ) {
+						return domWidgetElement;
+					},
+					// TODO consider other positions.
+					isCentered() {
+						const imageStyle = data.item.getAttribute( 'imageStyle' );
+
+						return !imageStyle || imageStyle == 'full' || imageStyle == 'alignCenter';
+					},
+
+					onCommit( newValue ) {
+						editor.execute( 'imageResize', { width: newValue } );
+					}
+				} );
+
+			resizer.on( 'updateSize', () => {
+				if ( !widget.hasClass( 'image_resized' ) ) {
+					editor.editing.view.change( writer => {
+						writer.addClass( 'image_resized', widget );
+					} );
+				}
+			} );
+
+			resizer.bind( 'isEnabled' ).to( command );
+		}, { priority: 'low' } );
+	}
+
+	/**
+	 * @private
+	 */
+	_registerSchema() {
+		this.editor.model.schema.extend( 'image', {
+			allowAttributes: 'width'
+		} );
+	}
+
+	/**
+	 * Registers image resize converters.
+	 *
+	 * @private
+	 */
+	_registerConverters() {
+		const editor = this.editor;
+
+		// Dedicated converter to propagate image's attribute to the img tag.
+		editor.conversion.for( 'downcast' ).add( dispatcher =>
+			dispatcher.on( 'attribute:width:image', ( evt, data, conversionApi ) => {
+				if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
+					return;
+				}
+
+				const viewWriter = conversionApi.writer;
+				const figure = conversionApi.mapper.toViewElement( data.item );
+
+				if ( data.attributeNewValue !== null ) {
+					viewWriter.setStyle( 'width', data.attributeNewValue, figure );
+					viewWriter.addClass( 'image_resized', figure );
+				} else {
+					viewWriter.removeStyle( 'width', figure );
+					viewWriter.removeClass( 'image_resized', figure );
+				}
+			} )
+		);
+
+		editor.conversion.for( 'upcast' )
+			.attributeToAttribute( {
+				view: {
+					name: 'figure',
+					styles: {
+						width: /.+/
+					}
+				},
+				model: {
+					key: 'width',
+					value: viewElement => viewElement.getStyle( 'width' )
+				}
+			} );
+	}
+}
+
+/**
+ * The available options are `'px'` or `'%'`.
+ *
+ * Determines size unit applied to resized image.
+ *
+ * ```js
+ * ClassicEditor
+ * 	.create( editorElement, {
+ * 		 image: {
+ * 			 resizeUnit: 'px'
+ * 		 }
+ * 	 } )
+ * 	.then( ... )
+ * 	.catch( ... );
+ * ```
+ *
+ * This option is used by the {@link module:image/imageresize~ImageResize} feature.
+ *
+ * @default '%'
+ * @member {String} module:image/image~ImageConfig#resizeUnit
+ */

+ 58 - 0
packages/ckeditor5-image/src/imageresize/imageresizecommand.js

@@ -0,0 +1,58 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module image/imageresize/imageresizecommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command';
+import { isImage } from '../image/utils';
+
+/**
+ * The image resize command. Currently, it supports only the width attribute.
+ *
+ * @extends module:core/command~Command
+ */
+export default class ImageResizeCommand extends Command {
+	/**
+	 * @inheritDoc
+	 */
+	refresh() {
+		const element = this.editor.model.document.selection.getSelectedElement();
+
+		this.isEnabled = isImage( element );
+
+		if ( !element || !element.hasAttribute( 'width' ) ) {
+			this.value = null;
+		} else {
+			this.value = {
+				width: element.getAttribute( 'width' ),
+				height: null
+			};
+		}
+	}
+
+	/**
+	 * Executes the command.
+	 *
+	 *		// Sets the width to 50%:
+	 *		editor.execute( 'imageResize', { width: '50%' } );
+	 *
+	 *		// Removes the width attribute:
+	 *		editor.execute( 'imageResize', { width: null } );
+	 *
+	 * @param {Object} options
+	 * @param {String|null} options.width The new width of the image.
+	 * @fires execute
+	 */
+	execute( options ) {
+		const model = this.editor.model;
+		const imageElement = model.document.selection.getSelectedElement();
+
+		model.change( writer => {
+			writer.setAttribute( 'width', options.width, imageElement );
+		} );
+	}
+}

+ 686 - 0
packages/ckeditor5-image/tests/imageresize.js

@@ -0,0 +1,686 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document, window */
+
+// ClassicTestEditor can't be used, as it doesn't handle the focus, which is needed to test resizer visual cues.
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Image from '../src/image';
+import ImageResize from '../src/imageresize';
+import ImageResizeCommand from '../src/imageresize/imageresizecommand';
+import ImageStyle from '../src/imagestyle';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import Table from '@ckeditor/ckeditor5-table/src/table';
+
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'ImageResize', () => {
+	// Id of the left mouse button.
+	const MOUSE_BUTTON_MAIN = 0;
+	// 60x50 black png image
+	const IMAGE_SRC_FIXTURE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAyCAQAAAAAPLY1AAAAQklEQVR42u3PQREAAAgDoK1/' +
+		'aM3g14MGNJMXKiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJysRFNMgH0RpujAAAAAElFTkSuQmCC';
+
+	let absoluteContainer, widget, editor, view, viewDocument, editorElement;
+
+	before( () => {
+		// This container is required to position editor element in a reliable element.
+		// See fireMouseEvent method for more information regarding imprecise mouse position.
+		absoluteContainer = document.createElement( 'div' );
+		absoluteContainer.style.top = '50px';
+		absoluteContainer.style.left = '50px';
+		absoluteContainer.style.height = '1000px';
+		absoluteContainer.style.width = '500px';
+		absoluteContainer.style.position = 'absolute';
+		document.body.appendChild( absoluteContainer );
+	} );
+
+	after( () => {
+		absoluteContainer.remove();
+	} );
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+
+		absoluteContainer.appendChild( editorElement );
+
+		return ClassicEditor
+			.create( editorElement, {
+				plugins: [ Image, ImageStyle, Paragraph, Undo, Table, ImageResize ],
+				image: {
+					resizeUnit: 'px'
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				view = editor.editing.view;
+				viewDocument = view.document;
+				widget = viewDocument.getRoot().getChild( 1 );
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'conversion', () => {
+		it( 'upcasts 100px width correctly', () => {
+			editor.setData( `<figure class="image" style="width:100px;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
+
+			expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'width' ) ).to.equal( '100px' );
+		} );
+
+		it( 'upcasts 50% width correctly', () => {
+			editor.setData( `<figure class="image" style="width:50%;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
+
+			expect( editor.model.document.getRoot().getChild( 0 ).getAttribute( 'width' ) ).to.equal( '50%' );
+		} );
+
+		it( 'downcasts 100px width correctly', () => {
+			setData( editor.model, `<image src="${ IMAGE_SRC_FIXTURE }" width="100px"></image>` );
+
+			expect( editor.getData() )
+				.to.equal( `<figure class="image image_resized" style="width:100px;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
+		} );
+
+		it( 'downcasts 50% width correctly', () => {
+			setData( editor.model, `<image src="${ IMAGE_SRC_FIXTURE }" width="50%"></image>` );
+
+			expect( editor.getData() )
+				.to.equal( `<figure class="image image_resized" style="width:50%;"><img src="${ IMAGE_SRC_FIXTURE }"></figure>` );
+		} );
+	} );
+
+	describe( 'schema', () => {
+		it( 'allows the width attribute', () => {
+			expect( editor.model.schema.checkAttribute( 'image', 'width' ) ).to.be.true;
+		} );
+
+		it( 'defines width as a formatting attribute', () => {
+			expect( editor.model.schema.getAttributeProperties( 'width' ) ).to.have.property( 'isFormatting', true );
+		} );
+	} );
+
+	describe( 'command', () => {
+		it( 'defines the imageResize command', () => {
+			expect( editor.commands.get( 'imageResize' ) ).to.be.instanceOf( ImageResizeCommand );
+		} );
+
+		it( 'uses the command on commit', async () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			widget = viewDocument.getRoot().getChild( 1 );
+
+			const spy = sinon.spy( editor.commands.get( 'imageResize' ), 'execute' );
+
+			await generateResizeTest( {
+				expectedWidth: 80,
+				pointerOffset: {
+					x: 10,
+					y: -10
+				},
+				resizerPosition: 'bottom-left'
+			} )();
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( spy.args[ 0 ][ 0 ] ).to.deep.equal( { width: '80px' } );
+		} );
+
+		it( 'disables the resizer if the command is disabled', () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			const resizer = editor.plugins.get( 'WidgetResize' ).resizers[ 0 ];
+
+			let isEnabled = false;
+
+			editor.commands.get( 'imageResize' ).on( 'set:isEnabled', evt => {
+				evt.return = isEnabled;
+				evt.stop();
+			}, { priority: 'highest' } );
+
+			editor.commands.get( 'imageResize' ).refresh();
+			expect( resizer.isEnabled ).to.be.false;
+
+			isEnabled = true;
+			editor.commands.get( 'imageResize' ).refresh();
+			expect( resizer.isEnabled ).to.be.true;
+		} );
+
+		it( 'the resizer is disabled from the beginning when the command is disabled when the image is inserted', () => {
+			setData( editor.model, '<paragraph>foo[]</paragraph>' );
+
+			editor.commands.get( 'imageResize' ).on( 'set:isEnabled', evt => {
+				evt.return = false;
+				evt.stop();
+			}, { priority: 'highest' } );
+			editor.commands.get( 'imageResize' ).refresh();
+
+			editor.model.change( writer => {
+				editor.model.insertContent( writer.createElement( 'image', { src: IMAGE_SRC_FIXTURE } ) );
+			} );
+
+			const resizer = editor.plugins.get( 'WidgetResize' ).resizers[ 0 ];
+			const resizerWrapper = editor.ui.getEditableElement().querySelector( '.ck-widget__resizer' );
+
+			expect( resizer.isEnabled ).to.be.false;
+			expect( resizerWrapper.style.display ).to.equal( 'none' );
+		} );
+	} );
+
+	describe( 'visual resizers', () => {
+		beforeEach( () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			widget = viewDocument.getRoot().getChild( 1 );
+		} );
+
+		it( 'correct number is added by default', () => {
+			const resizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
+
+			expect( resizers.length ).to.be.equal( 4 );
+		} );
+
+		describe( 'visibility', () => {
+			it( 'is hidden by default', () => {
+				const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
+
+				for ( const resizer of allResizers ) {
+					expect( isVisible( resizer ) ).to.be.false;
+				}
+			} );
+
+			it( 'is shown when image is focused', () => {
+				const widget = viewDocument.getRoot().getChild( 1 );
+				const allResizers = editor.ui.getEditableElement().querySelectorAll( '.ck-widget__resizer__handle' );
+				const domEventDataMock = {
+					target: widget,
+					preventDefault: sinon.spy()
+				};
+
+				focusEditor( editor );
+
+				viewDocument.fire( 'mousedown', domEventDataMock );
+
+				for ( const resizer of allResizers ) {
+					expect( isVisible( resizer ) ).to.be.true;
+				}
+			} );
+		} );
+	} );
+
+	describe( 'standard image resizing', () => {
+		beforeEach( () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			widget = viewDocument.getRoot().getChild( 1 );
+		} );
+
+		it( 'shrinks correctly with left-bottom handler', generateResizeTest( {
+			expectedWidth: 80,
+			pointerOffset: {
+				x: 10,
+				y: -10
+			},
+			resizerPosition: 'bottom-left'
+		} ) );
+
+		it( 'shrinks correctly with right-bottom handler', generateResizeTest( {
+			expectedWidth: 80,
+			pointerOffset: {
+				x: -10,
+				y: -10
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'enlarges correctly with right-bottom handler, x axis only', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 10,
+				y: 0
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'enlarges correctly with right-bottom handler, y axis only', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 0,
+				y: 10
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'enlarges correctly with left-bottom handler, x axis only', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: -10,
+				y: 0
+			},
+			resizerPosition: 'bottom-left'
+		} ) );
+
+		it( 'enlarges correctly with left-bottom handler, y axis only', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 0,
+				y: 10
+			},
+			resizerPosition: 'bottom-left'
+		} ) );
+
+		// --- top handlers ---
+
+		it( 'enlarges correctly with left-top handler', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: -10,
+				y: -10
+			},
+			resizerPosition: 'top-left'
+		} ) );
+
+		it( 'enlarges correctly with left-top handler, y axis only', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 0,
+				y: -10
+			},
+			resizerPosition: 'top-left'
+		} ) );
+
+		it( 'enlarges correctly with right-top handler', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 10,
+				y: -10
+			},
+			resizerPosition: 'top-right'
+		} ) );
+
+		it( 'enlarges correctly with right-top handler, y axis only', generateResizeTest( {
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 0,
+				y: -10
+			},
+			resizerPosition: 'top-right'
+		} ) );
+	} );
+
+	describe( 'side image resizing', () => {
+		beforeEach( () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image imageStyle="side" src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			widget = viewDocument.getRoot().getChild( 1 );
+		} );
+
+		it( 'shrinks correctly with left-bottom handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 80,
+			pointerOffset: {
+				x: 20,
+				y: -10
+			},
+			resizerPosition: 'bottom-left'
+		} ) );
+
+		it( 'shrinks correctly with right-bottom handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 80,
+			pointerOffset: {
+				x: -20,
+				y: -10
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'shrinks correctly with left-top handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 80,
+			pointerOffset: {
+				x: 20,
+				y: 10
+			},
+			resizerPosition: 'top-left'
+		} ) );
+
+		it( 'shrinks correctly with right-top handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 80,
+			pointerOffset: {
+				x: -20,
+				y: 10
+			},
+			resizerPosition: 'top-right'
+		} ) );
+
+		it( 'enlarges correctly with left-bottom handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 120,
+			pointerOffset: {
+				x: -10,
+				y: 10
+			},
+			resizerPosition: 'bottom-left'
+		} ) );
+
+		it( 'enlarges correctly with right-bottom handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 10,
+				y: 10
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'enlarges correctly with right-bottom handler, y axis only', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 140,
+			pointerOffset: {
+				x: 0,
+				y: 20
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'enlarges correctly with right-bottom handler, x axis only', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 140,
+			pointerOffset: {
+				x: 40,
+				y: 0
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'enlarges correctly with left-top handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 120,
+			pointerOffset: {
+				x: -20,
+				y: -10
+			},
+			resizerPosition: 'top-left'
+		} ) );
+
+		it( 'enlarges correctly with right-top handler', generateSideResizeTest( {
+			isSideImage: true,
+			expectedWidth: 120,
+			pointerOffset: {
+				x: 20,
+				y: 10
+			},
+			resizerPosition: 'top-right'
+		} ) );
+
+		function generateSideResizeTest( options ) {
+			return generateResizeTest( Object.assign( {
+				isSideImage: true,
+				modelRegExp: /<paragraph>foo<\/paragraph><image imageStyle="side" src=".+?" width="([\d.]+)px"><\/image>/
+			}, options ) );
+		}
+	} );
+
+	describe( 'undo integration', () => {
+		beforeEach( () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			widget = viewDocument.getRoot().getChild( 1 );
+		} );
+
+		it( 'has correct border size after undo', async () => {
+			await generateResizeTest( {
+				expectedWidth: 120,
+				pointerOffset: {
+					x: 0,
+					y: 10
+				},
+				resizerPosition: 'bottom-left'
+			} )();
+
+			editor.commands.get( 'undo' ).execute();
+
+			await wait( 40 );
+
+			const resizerWrapper = document.querySelector( '.ck-widget__resizer' );
+			const shadowBoundingRect = resizerWrapper.getBoundingClientRect();
+
+			expect( shadowBoundingRect.width ).to.be.equal( 100 );
+			expect( shadowBoundingRect.height ).to.be.equal( 50 );
+		} );
+	} );
+
+	describe( 'table integration', () => {
+		beforeEach( () => {
+			setData( editor.model,
+				'<table>' +
+					`<tableRow><tableCell>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]</tableCell></tableRow>` +
+				'</table>'
+			);
+
+			widget = viewDocument.getRoot().getChild( 0 ).getChild( 1 ).getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 );
+		} );
+
+		it( 'works when resizing in a table', generateResizeTest( {
+			getModel: () => editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ).getChild( 0 ),
+			expectedWidth: 60,
+			modelRegExp: /.+/,
+			pointerOffset: {
+				x: -40,
+				y: -20
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+	} );
+
+	describe( 'srcset integration', () => {
+		// The image is 96x96 pixels.
+		const imageBaseUrl = '/assets/sample.png';
+		const getModel = () => editor.model.document.getRoot().getChild( 0 );
+		let image;
+
+		before( async () => {
+			image = await preloadImage( imageBaseUrl );
+		} );
+
+		after( () => {
+			image.remove();
+		} );
+
+		beforeEach( () => {
+			editor.setData(
+				`<figure class="image">
+					<img src="${ imageBaseUrl }"
+						srcset="${ imageBaseUrl }?a 110w,
+							${ imageBaseUrl }?b 440w,
+							${ imageBaseUrl }?c 1025w"
+						sizes="100vw" width="96">
+				</figure>`
+			);
+
+			widget = viewDocument.getRoot().getChild( 0 );
+		} );
+
+		it( 'works with images containing srcset', generateResizeTest( {
+			getModel,
+			expectedWidth: 76,
+			modelRegExp: /.+/,
+			pointerOffset: {
+				x: -20,
+				y: -20
+			},
+			resizerPosition: 'bottom-right'
+		} ) );
+
+		it( 'retains width after removing srcset', async () => {
+			await generateResizeTest( {
+				getModel,
+				expectedWidth: 80,
+				modelRegExp: /.+/,
+				pointerOffset: {
+					x: -16,
+					y: -16
+				},
+				resizerPosition: 'bottom-right'
+			} )();
+
+			editor.model.change( writer => {
+				writer.removeAttribute( 'srcset', getModel() );
+			} );
+
+			expect( editor.getData() )
+				.to.match( /<figure class="image image_resized" style="width:[\d.]{2,}px;"><img src="\/assets\/sample.png"><\/figure>/ );
+		} );
+
+		async function preloadImage( imageUrl ) {
+			const image = document.createElement( 'img' );
+
+			image.src = imageUrl;
+
+			return new Promise( ( resolve, reject ) => {
+				image.addEventListener( 'load', () => resolve( image ) );
+				image.addEventListener( 'error', () => reject( image ) );
+				document.body.appendChild( image );
+			} );
+		}
+	} );
+
+	// TODO move to Resizer tests.
+	describe( 'Resizer', () => {
+		it( 'uses rounded (int) values', async () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			widget = viewDocument.getRoot().getChild( 1 );
+
+			await generateResizeTest( {
+				expectedWidth: 97,
+				// Makes it resize the image to 97.2188px, unless there's a rounding.
+				pointerOffset: {
+					x: 7.3,
+					y: -1
+				},
+				resizerPosition: 'bottom-left',
+				checkBeforeMouseUp( domFigure, domResizeWrapper ) {
+					expect( domFigure.style.width ).to.match( /^\d\dpx$/ );
+					expect( domResizeWrapper.style.width ).to.match( /^\d\dpx$/ );
+				}
+			} )();
+
+			expect( editor.model.document.getRoot().getChild( 1 ).getAttribute( 'width' ) ).to.match( /^\d\dpx$/ );
+		} );
+
+		it( 'hides the resize wrapper when its disabled', () => {
+			setData( editor.model, `<paragraph>foo</paragraph>[<image src="${ IMAGE_SRC_FIXTURE }"></image>]` );
+
+			const resizer = editor.plugins.get( 'WidgetResize' ).resizers[ 0 ];
+			const resizerWrapper = editor.ui.getEditableElement().querySelector( '.ck-widget__resizer' );
+
+			expect( resizerWrapper.style.display ).to.equal( '' );
+
+			resizer.isEnabled = false;
+
+			expect( resizerWrapper.style.display ).to.equal( 'none' );
+		} );
+	} );
+
+	function isVisible( element ) {
+		// Checks if the DOM element is visible to the end user.
+		return element.offsetParent !== null && !element.classList.contains( 'ck-hidden' );
+	}
+
+	function fireMouseEvent( target, eventType, eventData ) {
+		// Using initMouseEvent instead of MouseEvent constructor, as MouseEvent constructor doesn't support passing pageX
+		// and pageY. See https://stackoverflow.com/questions/45843458/setting-click-events-pagex-and-pagey-always-reverts-to-0
+		// However there's still a problem, that events created with `initMouseEvent` have **floored** pageX, pageY numbers.
+		const event = document.createEvent( 'MouseEvent' );
+		event.initMouseEvent( eventType, true, true, window, null, 0, 0, eventData.pageX, eventData.pageY, false, false, false, false,
+			MOUSE_BUTTON_MAIN, null );
+
+		target.dispatchEvent( event );
+	}
+
+	function wait( delay ) {
+		return new Promise( resolve => window.setTimeout( () => resolve(), delay ) );
+	}
+
+	function generateResizeTest( options ) {
+		// options.resizerPosition - top-left / top-right / bottom-right / bottom-left
+		// options.pointerOffset - object - pointer offset relative to the dragged corner. Negative values are perfectly fine.
+		// e.g. { x: 10, y: -5 }
+		// options.expectedWidth
+		// [options.isSideImage=false]
+		// Returns a test case that puts
+		return function() {
+			const domResizeWrapper = view.domConverter.mapViewToDom( widget.getChild( 1 ) );
+			const domResizeHandle = domResizeWrapper.querySelector( `.ck-widget__resizer__handle-${ options.resizerPosition }` );
+			const domFigure = view.domConverter.mapViewToDom( widget );
+			const domImage = domFigure.querySelector( 'img' );
+			const imageRect = new Rect( domImage );
+			const resizerPositionParts = options.resizerPosition.split( '-' );
+
+			const modelRegExp = options.modelRegExp ? options.modelRegExp :
+				/<paragraph>foo<\/paragraph><image src=".+?" width="([\d]+)px"><\/image>/;
+
+			focusEditor( editor );
+
+			const initialPointerPosition = {
+				pageX: imageRect.left,
+				pageY: imageRect.top
+			};
+
+			if ( resizerPositionParts.includes( 'right' ) ) {
+				initialPointerPosition.pageX = imageRect.right;
+			}
+
+			if ( resizerPositionParts.includes( 'bottom' ) ) {
+				initialPointerPosition.pageY = imageRect.bottom;
+			}
+
+			const finishPointerPosition = Object.assign( {}, initialPointerPosition );
+
+			finishPointerPosition.pageX += options.pointerOffset.x || 0;
+			finishPointerPosition.pageY += options.pointerOffset.y || 0;
+
+			fireMouseEvent( domResizeHandle, 'mousedown', initialPointerPosition );
+			fireMouseEvent( domResizeHandle, 'mousemove', initialPointerPosition );
+
+			// We need to wait as mousemove events are throttled.
+			return wait( 40 )
+				.then( () => {
+					fireMouseEvent( domResizeHandle, 'mousemove', finishPointerPosition );
+
+					expect( parseInt( domFigure.style.width ) ).to.be.closeTo( options.expectedWidth, 2, 'DOM width check' );
+
+					if ( options.checkBeforeMouseUp ) {
+						options.checkBeforeMouseUp( domFigure, domResizeWrapper );
+					}
+
+					fireMouseEvent( domResizeHandle, 'mouseup', finishPointerPosition );
+
+					expect( getData( editor.model, {
+						withoutSelection: true
+					} ) ).to.match( modelRegExp );
+
+					const modelItem = options.getModel ? options.getModel() : editor.model.document.getRoot().getChild( 1 );
+					const modelWidth = modelItem.getAttribute( 'width' );
+
+					expect( parseFloat( modelWidth, 0 ) )
+						.to.be.closeTo( options.expectedWidth, 2, 'Model width check' );
+				} );
+		};
+	}
+
+	function focusEditor( editor ) {
+		editor.editing.view.focus();
+		editor.ui.focusTracker.isFocused = true;
+	}
+} );

+ 88 - 0
packages/ckeditor5-image/tests/imageresize/imageresizecommand.js

@@ -0,0 +1,88 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import ImageResizeCommand from '../../src/imageresize/imageresizecommand';
+import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'ImageStyleCommand', () => {
+	let model, command;
+
+	beforeEach( () => {
+		return ModelTestEditor.create()
+			.then( newEditor => {
+				model = newEditor.model;
+				command = new ImageResizeCommand( newEditor );
+
+				model.schema.register( 'p', { inheritAllFrom: '$block' } );
+
+				model.schema.register( 'image', {
+					isObject: true,
+					isBlock: true,
+					allowWhere: '$block',
+					allowAttributes: 'width'
+				} );
+			} );
+	} );
+
+	describe( '#isEnabled', () => {
+		it( 'is true when image is selected', () => {
+			setData( model, '<p>x</p>[<image width="50px"></image>]<p>x</p>' );
+
+			expect( command ).to.have.property( 'isEnabled', true );
+		} );
+
+		it( 'is false when image is not selected', () => {
+			setData( model, '<p>x[]</p><image width="50px"></image>' );
+
+			expect( command ).to.have.property( 'isEnabled', false );
+		} );
+
+		it( 'is false when more than one image is selected', () => {
+			setData( model, '<p>x</p>[<image width="50px"></image><image width="50px"></image>]' );
+
+			expect( command ).to.have.property( 'isEnabled', false );
+		} );
+	} );
+
+	describe( '#value', () => {
+		it( 'is null when image is not selected', () => {
+			setData( model, '<p>x[]</p><image width="50px"></image>' );
+
+			expect( command ).to.have.property( 'value', null );
+		} );
+
+		it( 'is set to an object with a width property (and height set to null)', () => {
+			setData( model, '<p>x</p>[<image width="50px"></image>]<p>x</p>' );
+
+			expect( command ).to.have.deep.property( 'value', { width: '50px', height: null } );
+		} );
+
+		it( 'is set to null if image does not have the width set', () => {
+			setData( model, '<p>x</p>[<image></image>]<p>x</p>' );
+
+			expect( command ).to.have.property( 'value', null );
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'sets image width', () => {
+			setData( model, '[<image width="50px"></image>]' );
+
+			command.execute( { width: '100%' } );
+
+			expect( getData( model ) ).to.equal( '[<image width="100%"></image>]' );
+		} );
+
+		it( 'removes image width when null passed', () => {
+			setData( model, '[<image width="50px"></image>]' );
+
+			command.execute( { width: null } );
+
+			expect( getData( model ) ).to.equal( '[<image></image>]' );
+			expect( model.document.getRoot().getChild( 0 ).hasAttribute( 'width' ) ).to.be.false;
+		} );
+	} );
+} );

+ 79 - 0
packages/ckeditor5-image/tests/manual/imageresize.html

@@ -0,0 +1,79 @@
+<style>
+	.fancy-editor-wrapper figure {
+		padding: 20px;
+	}
+</style>
+
+<div id="editor">
+	<h2>Image (width: 40%)</h2>
+
+	<figure class="image image-style-side" style="width: 40%">
+		<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. </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>
+
+	<h2>Image (width: not set)</h2>
+
+	<figure class="image">
+		<img src="sample.jpg" alt="" />
+	</figure>
+
+	<h2>Responsive image (width style: not set, width attribute present on the image)</h2>
+
+	<figure class="image">
+		<img alt="Picture of the Warsaw Old Town." width="4288" srcset="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_430 430w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_860 860w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1290 1290w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1720 1720w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2150 2150w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2580 2580w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3010 3010w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3440 3440w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3870 3870w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_4288 4288w" sizes="100vw" src="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg">
+	</figure>
+
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id telluset mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortisleo interdum. </p>
+</div>
+
+<h1>Fancy editor (padding inside figure)</h1>
+
+<div class="fancy-editor-wrapper">
+	<div id="fancy-editor">
+		<h2>Image (width: 40%)</h2>
+
+		<figure class="image image-style-side" style="width: 40%">
+			<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. </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>
+
+		<h2>Image (width: not set)</h2>
+
+		<figure class="image">
+			<img src="sample.jpg" alt="" />
+		</figure>
+
+		<h2>Responsive image (width style: not set, width attribute present on the image)</h2>
+
+		<figure class="image">
+			<img alt="Picture of the Warsaw Old Town." width="4288" srcset="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_430 430w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_860 860w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1290 1290w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1720 1720w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2150 2150w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2580 2580w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3010 3010w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3440 3440w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3870 3870w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_4288 4288w" sizes="100vw" src="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg">
+		</figure>
+
+		<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id telluset mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortisleo interdum. </p>
+	</div>
+</div>
+
+<h1>Images with widths defined with other units</h1>
+
+<div id="editor-other-units">
+	<h2>Image (width: 200px)</h2>
+
+	<figure class="image" style="width: 200px">
+		<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.</p>
+
+	<h2>Image (width: 10em)</h2>
+
+	<figure class="image" style="width: 10em">
+		<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.</p>
+</div>

+ 63 - 0
packages/ckeditor5-image/tests/manual/imageresize.js

@@ -0,0 +1,63 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document, console, window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import ImageResize from '../../src/imageresize';
+import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
+import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+const commonConfig = {
+	plugins: [
+		ArticlePluginSet,
+		ImageResize,
+		Indent,
+		IndentBlock,
+		EasyImage
+	],
+	toolbar: [ 'heading', '|', 'bold', 'italic', 'link',
+		'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'undo', 'redo', 'outdent', 'indent' ],
+	image: {
+		toolbar: [ 'imageStyle:alignLeft', 'imageStyle:full', 'imageStyle:side' ],
+		styles: [
+			'full',
+			'alignLeft',
+			'side' // Purposely using side image instead right aligned image to make sure it works well with both style types.
+		]
+	},
+	cloudServices: CS_CONFIG
+};
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), commonConfig )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+ClassicEditor
+	.create( document.querySelector( '#fancy-editor' ), commonConfig )
+	.then( editor => {
+		window.fancyEditor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+ClassicEditor
+	.create( document.querySelector( '#editor-other-units' ), commonConfig )
+	.then( editor => {
+		window.editorOtherUnits = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 10 - 0
packages/ckeditor5-image/tests/manual/imageresize.md

@@ -0,0 +1,10 @@
+## Image resize
+
+* Image resizer should be visible after focusing the image.
+* There should be four resizers.
+* Resizers should always be bound to image, not the entire image widget.
+
+### Edge cases
+
+* Make sure that the resized image can't be bigger than the editable.
+* Make sure that releasing mouse button outside of an editable stops the resize.

+ 83 - 0
packages/ckeditor5-image/tests/manual/imageresizepx.html

@@ -0,0 +1,83 @@
+<style>
+	.fancy-editor-wrapper figure {
+		padding: 20px;
+	}
+</style>
+
+<div id="editor">
+	<h2>Image (width: 200px)</h2>
+
+	<figure class="image image-style-side" style="width: 200px">
+		<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.</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>
+
+	<h2>Image (width: not set)</h2>
+
+	<figure class="image">
+		<img src="sample.jpg" alt="" />
+	</figure>
+
+	<h2>Responsive image (width style: not set, width attribute present on the image)</h2>
+
+	<figure class="image">
+		<img alt="Picture of the Warsaw Old Town." width="4288" srcset="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_430 430w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_860 860w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1290 1290w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1720 1720w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2150 2150w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2580 2580w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3010 3010w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3440 3440w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3870 3870w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_4288 4288w" sizes="100vw" src="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg">
+	</figure>
+
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id telluset mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.</p>
+</div>
+
+<h1>Images with widths defined with other units</h1>
+
+<div id="editor-other-units">
+	<h2>Image (width: 40%)</h2>
+
+	<figure class="image" style="width: 40%">
+		<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.</p>
+
+	<h2>Image (width: 10em)</h2>
+
+	<figure class="image" style="width: 10em">
+		<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.</p>
+</div>
+
+<div style="background:red; padding: 10px">
+	<h1>Fancy editor (padding inside figure)</h1>
+
+	<p>Note: this feature doesn't well with padding and px units for now.</p>
+
+	<div class="fancy-editor-wrapper" style="background:red">
+		<div id="fancy-editor">
+			<h2>Image (width: 200px)</h2>
+
+			<figure class="image image-style-side" style="width: 200px">
+				<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. </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>
+
+			<h2>Image (width: not set)</h2>
+
+			<figure class="image">
+				<img src="sample.jpg" alt="" />
+			</figure>
+
+			<h2>Responsive image (width style: not set, width attribute present on the image)</h2>
+
+			<figure class="image">
+				<img alt="Picture of the Warsaw Old Town." width="4288" srcset="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_430 430w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_860 860w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1290 1290w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_1720 1720w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2150 2150w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_2580 2580w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3010 3010w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3440 3440w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_3870 3870w, https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg/w_4288 4288w" sizes="100vw" src="https://33333.cdn.cke-cs.com/0fNqCUeBSal4ENvGfd7K/images/c12cb88e596270d8d963b7c87bef1767880f42d94fb2ec43_warsaw01.jpg">
+			</figure>
+
+			<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id telluset mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortisleo interdum. </p>
+		</div>
+	</div>
+</div>

+ 64 - 0
packages/ckeditor5-image/tests/manual/imageresizepx.js

@@ -0,0 +1,64 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global document, console, window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import ImageResize from '../../src/imageresize';
+import Indent from '@ckeditor/ckeditor5-indent/src/indent';
+import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock';
+import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+const commonConfig = {
+	plugins: [
+		ArticlePluginSet,
+		ImageResize,
+		Indent,
+		IndentBlock,
+		EasyImage
+	],
+	toolbar: [ 'heading', '|', 'bold', 'italic', 'link',
+		'bulletedList', 'numberedList', 'blockQuote', 'insertTable', 'undo', 'redo', 'outdent', 'indent' ],
+	image: {
+		resizeUnit: 'px',
+		toolbar: [ 'imageStyle:alignLeft', 'imageStyle:full', 'imageStyle:side' ],
+		styles: [
+			'full',
+			'alignLeft',
+			'side' // Purposely using side image instead right aligned image to make sure it works well with both style types.
+		]
+	},
+	cloudServices: CS_CONFIG
+};
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), commonConfig )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+ClassicEditor
+	.create( document.querySelector( '#fancy-editor' ), commonConfig )
+	.then( editor => {
+		window.fancyEditor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+ClassicEditor
+	.create( document.querySelector( '#editor-other-units' ), commonConfig )
+	.then( editor => {
+		window.editorOtherUnits = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 10 - 0
packages/ckeditor5-image/tests/manual/imageresizepx.md

@@ -0,0 +1,10 @@
+## Image resize
+
+* Image resizer should be visible after focusing the image.
+* There should be four resizers.
+* Resizers should always be bound to image, not the entire image widget.
+
+### Edge cases
+
+* Make sure that the resized image can't be bigger than the editable.
+* Make sure that releasing mouse button outside of an editable stops the resize.

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

@@ -6,28 +6,12 @@
 /* global document, console, window */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-import EnterPlugin from '@ckeditor/ckeditor5-enter/src/enter';
-import TypingPlugin from '@ckeditor/ckeditor5-typing/src/typing';
-import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import HeadingPlugin from '@ckeditor/ckeditor5-heading/src/heading';
-import ImagePlugin from '../../src/image';
-import UndoPlugin from '@ckeditor/ckeditor5-undo/src/undo';
-import ClipboardPlugin from '@ckeditor/ckeditor5-clipboard/src/clipboard';
-import ImageStyle from '../../src/imagestyle';
-import ImageToolbar from '../../src/imagetoolbar';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 
 ClassicEditor
 	.create( document.querySelector( '#editor-semantic' ), {
 		plugins: [
-			ImageToolbar,
-			EnterPlugin,
-			TypingPlugin,
-			ParagraphPlugin,
-			HeadingPlugin,
-			ImagePlugin,
-			UndoPlugin,
-			ClipboardPlugin,
-			ImageStyle
+			ArticlePluginSet
 		],
 		toolbar: [ 'heading', '|', 'undo', 'redo' ],
 		image: {
@@ -44,15 +28,7 @@ ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor-formatting' ), {
 		plugins: [
-			ImageToolbar,
-			EnterPlugin,
-			TypingPlugin,
-			ParagraphPlugin,
-			HeadingPlugin,
-			ImagePlugin,
-			UndoPlugin,
-			ClipboardPlugin,
-			ImageStyle
+			ArticlePluginSet
 		],
 		toolbar: [ 'heading', '|', 'undo', 'redo' ],
 		image: {

+ 25 - 0
packages/ckeditor5-image/theme/imageresize.css

@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck-content .image.image_resized {
+	max-width: 100%;
+	/*
+	The figure element for resized images must not use `display:table` as browsers doesn't support `max-width` for it well.
+	See https://stackoverflow.com/questions/4019604/chrome-safari-ignoring-max-width-in-table/14420691#14420691 for more.
+	Fortunately, since we control width, there's no risk that the image will look bad.
+	*/
+	display: block;
+	box-sizing: border-box;
+
+	& img {
+		/* For resized images it's the figure that determines the image's width. */
+		width: 100%;
+	}
+
+	&>figcaption {
+		/* Figure uses display block, so figcaption also has to. */
+		display: block;
+	}
+}

+ 3 - 1
packages/ckeditor5-image/theme/imagestyle.css

@@ -12,7 +12,9 @@
 	& .image-style-align-left,
 	& .image-style-align-center,
 	& .image-style-align-right {
-		max-width: 50%;
+		&:not(.image_resized) {
+			max-width: 50%;
+		}
 	}
 
 	& .image-style-side {

+ 0 - 1
packages/ckeditor5-image/theme/imageuploadprogress.css

@@ -5,7 +5,6 @@
 
 .ck-content .image {
 	position: relative;
-	overflow: hidden;
 }
 
 /* Upload progress bar. */