8
0
Просмотр исходного кода

Merge branch 'master' into t/9

Szymon Kupś 9 лет назад
Родитель
Сommit
e9fe1fd978
48 измененных файлов с 1057 добавлено и 194 удалено
  1. 2 2
      packages/ckeditor5-image/lang/contexts.json
  2. 4 4
      packages/ckeditor5-image/src/image.js
  3. 11 1
      packages/ckeditor5-image/src/image/converters.js
  4. 2 2
      packages/ckeditor5-image/src/image/imageengine.js
  5. 15 11
      packages/ckeditor5-image/src/image/ui/imageballoonpanelview.js
  6. 2 2
      packages/ckeditor5-image/src/image/utils.js
  7. 0 26
      packages/ckeditor5-image/src/imagealternatetext/imagealternatetextengine.js
  8. 26 0
      packages/ckeditor5-image/src/imagecaption.js
  9. 225 0
      packages/ckeditor5-image/src/imagecaption/imagecaptionengine.js
  10. 63 0
      packages/ckeditor5-image/src/imagecaption/utils.js
  11. 2 2
      packages/ckeditor5-image/src/imagestyle.js
  12. 1 1
      packages/ckeditor5-image/src/imagestyle/converters.js
  13. 1 1
      packages/ckeditor5-image/src/imagestyle/imagestylecommand.js
  14. 1 1
      packages/ckeditor5-image/src/imagestyle/imagestyleengine.js
  15. 23 23
      packages/ckeditor5-image/src/imagetextalternative.js
  16. 4 4
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativecommand.js
  17. 26 0
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeengine.js
  18. 6 6
      packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js
  19. 3 3
      packages/ckeditor5-image/src/imagetoolbar.js
  20. 28 5
      packages/ckeditor5-image/src/widget/widget.js
  21. 2 0
      packages/ckeditor5-image/src/widget/widgetengine.js
  22. 4 4
      packages/ckeditor5-image/tests/image.js
  23. 15 31
      packages/ckeditor5-image/tests/image/converters.js
  24. 11 2
      packages/ckeditor5-image/tests/image/imageengine.js
  25. 2 2
      packages/ckeditor5-image/tests/image/ui/imageballoonpanelview.js
  26. 2 2
      packages/ckeditor5-image/tests/image/utils.js
  27. 34 0
      packages/ckeditor5-image/tests/imagecaption.js
  28. 288 0
      packages/ckeditor5-image/tests/imagecaption/imagecaptionengine.js
  29. 68 0
      packages/ckeditor5-image/tests/imagecaption/utils.js
  30. 3 3
      packages/ckeditor5-image/tests/imagestyle.js
  31. 1 1
      packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js
  32. 24 24
      packages/ckeditor5-image/tests/imagetextalternative.js
  33. 3 3
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativecommand.js
  34. 5 5
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeengine.js
  35. 4 4
      packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js
  36. 1 1
      packages/ckeditor5-image/tests/imagetoolbar.js
  37. 0 10
      packages/ckeditor5-image/tests/manual/alternatetext.md
  38. 11 0
      packages/ckeditor5-image/tests/manual/caption.html
  39. 35 0
      packages/ckeditor5-image/tests/manual/caption.js
  40. 11 0
      packages/ckeditor5-image/tests/manual/caption.md
  41. 7 7
      packages/ckeditor5-image/tests/manual/imagestyle.js
  42. 0 0
      packages/ckeditor5-image/tests/manual/textalternative.html
  43. 0 0
      packages/ckeditor5-image/tests/manual/textalternative.js
  44. 10 0
      packages/ckeditor5-image/tests/manual/textalternative.md
  45. 23 0
      packages/ckeditor5-image/tests/widget/widget.js
  46. 20 0
      packages/ckeditor5-image/tests/widget/widgetengine.js
  47. 27 0
      packages/ckeditor5-image/theme/imagecaption/theme.scss
  48. 1 1
      packages/ckeditor5-image/theme/imagetextalternative/theme.scss

+ 2 - 2
packages/ckeditor5-image/lang/contexts.json

@@ -2,6 +2,6 @@
 	"image widget": "Label for the image widget.",
 	"Side image": "Label for the Side image option.",
 	"Full size image": "Label for the Full size image option.",
-	"Change alternate text": "Label for the Change alternate text button.",
-	"Alternate image text": "Label for the Alternate image text option."
+	"Change image text alternative": "Label for the Change image text alternative button.",
+	"Text alternative": "Label for the image text alternative input."
 }

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

@@ -8,16 +8,16 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageEngine from './imageengine';
+import ImageEngine from './image/imageengine';
 import Widget from './widget/widget';
-import ImageAlternateText from './imagealternatetext/imagealternatetext';
+import ImageTextAlternative from './imagetextalternative';
 
 import '../theme/theme.scss';
 
 /**
  * The image plugin.
  *
- * Uses {@link module:image/imageengine~ImageEngine}.
+ * Uses {@link module:image/image/imageengine~ImageEngine}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -26,6 +26,6 @@ export default class Image extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageEngine, Widget, ImageAlternateText ];
+		return [ ImageEngine, Widget, ImageTextAlternative ];
 	}
 }

+ 11 - 1
packages/ckeditor5-image/src/converters.js → packages/ckeditor5-image/src/image/converters.js

@@ -4,10 +4,12 @@
  */
 
 /**
- * @module image/converters
+ * @module image/image/converters
  */
 
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
+import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
 
 /**
  * Returns function that converts image view representation:
@@ -56,6 +58,14 @@ export function viewToModelImage() {
 			modelImage.setAttribute( 'alt', viewImg.getAttribute( 'alt' ) );
 		}
 
+		// Convert children of converted view element and append them to `modelImage`.
+		// TODO https://github.com/ckeditor/ckeditor5-engine/issues/736.
+		data.context.push( modelImage );
+		const modelChildren = conversionApi.convertChildren( viewFigureElement, consumable, data );
+		const insertPosition = ModelPosition.createAt( modelImage, 'end' );
+		modelWriter.insert( insertPosition, modelChildren );
+		data.context.pop();
+
 		data.output = modelImage;
 	};
 }

+ 2 - 2
packages/ckeditor5-image/src/imageengine.js → packages/ckeditor5-image/src/image/imageengine.js

@@ -4,12 +4,12 @@
  */
 
 /**
- * @module image/imageengine
+ * @module image/image/imageengine
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import WidgetEngine from './widget/widgetengine';
+import WidgetEngine from '../widget/widgetengine';
 import { viewToModelImage, createImageAttributeConverter } from './converters';
 import { toImageWidget } from './utils';
 import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';

+ 15 - 11
packages/ckeditor5-image/src/ui/imageballoonpanelview.js → packages/ckeditor5-image/src/image/ui/imageballoonpanelview.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module image/ui/imageballoonpanel
+ * @module image/image/ui/imageballoonpanel
  */
 
 import throttle from '@ckeditor/ckeditor5-utils/src/lib/lodash/throttle';
@@ -19,22 +19,26 @@ const positions = {
 	//	+-----------------+
 	//	|     Balloon     |
 	//	+-----------------+
-	south: ( targetRect, balloonRect ) => ( {
-		top: targetRect.bottom + arrowVOffset,
-		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
-		name: 's'
-	} ),
+	south( targetRect, balloonRect ) {
+		return {
+			top: targetRect.bottom + arrowVOffset,
+			left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
+			name: 's'
+		};
+	},
 
 	//	+-----------------+
 	//	|     Balloon     |
 	//	+-----------------+
 	//	        V
 	//	   [text range]
-	north: ( targetRect, balloonRect ) => ( {
-		top: targetRect.top - balloonRect.height - arrowVOffset,
-		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
-		name: 'n'
-	} )
+	north( targetRect, balloonRect ) {
+		return {
+			top: targetRect.top - balloonRect.height - arrowVOffset,
+			left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
+			name: 'n'
+		};
+	}
 };
 
 /**

+ 2 - 2
packages/ckeditor5-image/src/utils.js → packages/ckeditor5-image/src/image/utils.js

@@ -4,10 +4,10 @@
  */
 
 /**
- * @module image/utils
+ * @module image/image/utils
  */
 
-import { widgetize, isWidget, setFakeSelectionLabel } from './widget/utils';
+import { widgetize, isWidget, setFakeSelectionLabel } from '../widget/utils';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 
 const imageSymbol = Symbol( 'isImage' );

+ 0 - 26
packages/ckeditor5-image/src/imagealternatetext/imagealternatetextengine.js

@@ -1,26 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module image/imagealternatetext/imagealternatetextengine
- */
-
-import ImageAlternateTextCommand from './imagealternatetextcommand';
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-
-/**
- * The ImageAlternateTextEngine plugin.
- * Registers `imageAlternateText` command.
- *
- * @extends module:core/plugin~Plugin
- */
-export default class ImageAlternateTextEngine extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		this.editor.commands.set( 'imageAlternateText', new ImageAlternateTextCommand( this.editor ) );
-	}
-}

+ 26 - 0
packages/ckeditor5-image/src/imagecaption.js

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagecaption
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ImageCaptionEngine from './imagecaption/imagecaptionengine';
+import '../theme/imagecaption/theme.scss';
+
+/**
+ * The image caption plugin.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageCaption extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ImageCaptionEngine ];
+	}
+}

+ 225 - 0
packages/ckeditor5-image/src/imagecaption/imagecaptionengine.js

@@ -0,0 +1,225 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagecaption/imagecaptionengine
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ModelTreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
+import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
+import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
+import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
+import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
+import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
+import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
+import ViewMatcher from '@ckeditor/ckeditor5-engine/src/view/matcher';
+import { isImage, isImageWidget } from '../image/utils';
+import { captionElementCreator, isCaption, getCaptionFromImage } from './utils';
+
+/**
+ * The image caption engine plugin.
+ *
+ * Registers proper converters. Takes care of adding caption element if image without it is inserted to model document.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageCaptionEngine extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const document = editor.document;
+		const viewDocument = editor.editing.view;
+		const schema = document.schema;
+		const data = editor.data;
+		const editing = editor.editing;
+
+		/**
+		 * Last selected caption editable.
+		 * It is used for hiding editable when is empty and image widget is no longer selected.
+		 *
+		 * @member {module:image/imagecaption/imagecaptionengine~ImageCaptionEngine} #_lastSelectedEditable
+		 */
+
+		// Schema configuration.
+		schema.registerItem( 'caption' );
+		schema.allow( { name: '$inline', inside: 'caption' } );
+		schema.allow( { name: 'caption', inside: 'image' } );
+		schema.limits.add( 'caption' );
+
+		// Add caption element to each image inserted without it.
+		document.on( 'change', insertMissingCaptionElement );
+
+		// View to model converter for data pipeline.
+		const matcher = new ViewMatcher( ( element ) => {
+			const parent = element.parent;
+
+			// Convert only captions for images.
+			if ( element.name == 'figcaption' && parent && parent.name == 'figure' && parent.hasClass( 'image' ) ) {
+				return { name: true };
+			}
+
+			return null;
+		} );
+
+		buildViewConverter()
+			.for( data.viewToModel )
+			.from( matcher )
+			.toElement( 'caption' );
+
+		// Model to view converter for data pipeline.
+		data.modelToView.on(
+			'insert:caption',
+			captionModelToView( new ViewContainerElement( 'figcaption' ) )
+		);
+
+		// Model to view converter for editing pipeline.
+		editing.modelToView.on(
+			'insert:caption',
+			captionModelToView( captionElementCreator( viewDocument ) )
+		);
+
+		// Adding / removing caption element when there is no text in the model.
+		const selection = viewDocument.selection;
+
+		// Update view before each rendering.
+		this.listenTo( viewDocument, 'render', () => {
+			// Check if there is an empty caption view element to remove.
+			this._removeEmptyCaption();
+
+			// Check if image widget is selected and caption view element needs to be added.
+			this._addCaption();
+
+			// If selection is currently inside caption editable - store it to hide when empty.
+			const editableElement = selection.editableElement;
+
+			if ( editableElement && isCaption( selection.editableElement ) ) {
+				this._lastSelectedEditable = selection.editableElement;
+			}
+		}, { priority: 'high' } );
+	}
+
+	/**
+	 * Checks if there is an empty caption element to remove from view.
+	 *
+	 * @private
+	 */
+	_removeEmptyCaption() {
+		const viewSelection = this.editor.editing.view.selection;
+		const viewCaptionElement = this._lastSelectedEditable;
+
+		// No caption to hide.
+		if ( !viewCaptionElement ) {
+			return;
+		}
+
+		// If selection is placed inside caption - do not remove it.
+		if ( viewSelection.editableElement === viewCaptionElement ) {
+			return;
+		}
+
+		// Do not remove caption if selection is placed on image that contains that caption.
+		const selectedElement = viewSelection.getSelectedElement();
+
+		if ( selectedElement && isImageWidget( selectedElement ) ) {
+			const viewImage = viewCaptionElement.findAncestor( element => element == selectedElement );
+
+			if ( viewImage ) {
+				return;
+			}
+		}
+
+		// Remove image caption if its empty.
+		if ( viewCaptionElement.childCount === 0 ) {
+			const mapper = this.editor.editing.mapper;
+			viewWriter.remove( ViewRange.createOn( viewCaptionElement ) );
+			mapper.unbindViewElement( viewCaptionElement );
+		}
+	}
+
+	/**
+	 * Checks if selected image needs a new caption element inside.
+	 *
+	 * @private
+	 */
+	_addCaption() {
+		const editing = this.editor.editing;
+		const selection = editing.view.selection;
+		const imageFigure = selection.getSelectedElement();
+		const mapper = editing.mapper;
+		const editableCreator = captionElementCreator( editing.view );
+
+		if ( imageFigure && isImageWidget( imageFigure ) ) {
+			const modelImage = mapper.toModelElement( imageFigure );
+			const modelCaption = getCaptionFromImage( modelImage );
+			let viewCaption =  mapper.toViewElement( modelCaption );
+
+			if ( !viewCaption ) {
+				viewCaption = editableCreator();
+
+				const viewPosition = ViewPosition.createAt( imageFigure, 'end' );
+				mapper.bindElements( modelCaption, viewCaption );
+				viewWriter.insert( viewPosition, viewCaption );
+			}
+
+			this._lastSelectedEditable = viewCaption;
+		}
+	}
+}
+
+// Checks whether data inserted to the model document have image element that has no caption element inside it.
+// If there is none - adds it to the image element.
+//
+// @private
+function insertMissingCaptionElement( evt, changeType, data, batch ) {
+	if ( changeType !== 'insert' ) {
+		return;
+	}
+
+	const walker = new ModelTreeWalker( {
+		boundaries: data.range,
+		ignoreElementEnd: true
+	} );
+
+	for ( let value of walker ) {
+		const item = value.item;
+
+		if ( value.type == 'elementStart' && isImage( item ) && !getCaptionFromImage( item ) ) {
+			batch.document.enqueueChanges( () => {
+				batch.insert( ModelPosition.createAt( item, 'end' ), new ModelElement( 'caption' ) );
+			} );
+		}
+	}
+}
+
+// Creates a converter that converts image caption model element to view element.
+//
+// @private
+// @param {Function|module:engine/view/element~Element} elementCreator
+// @return {Function}
+function captionModelToView( elementCreator ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		const captionElement = data.item;
+
+		if ( isImage( captionElement.parent ) && ( captionElement.childCount > 0 ) ) {
+			if ( !consumable.consume( data.item, 'insert' ) ) {
+				return;
+			}
+
+			const imageFigure = conversionApi.mapper.toViewElement( data.range.start.parent );
+			const viewElement = ( elementCreator instanceof ViewElement ) ?
+				elementCreator.clone( true ) :
+				elementCreator( data, consumable, conversionApi );
+
+			const viewPosition = ViewPosition.createAt( imageFigure, 'end' );
+			conversionApi.mapper.bindElements( data.item, viewElement );
+			viewWriter.insert( viewPosition, viewElement );
+		}
+	};
+}

+ 63 - 0
packages/ckeditor5-image/src/imagecaption/utils.js

@@ -0,0 +1,63 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagecaption/utils
+ */
+
+import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
+import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+
+const captionSymbol = Symbol( 'imageCaption' );
+
+/**
+ * Returns a function that creates caption editable element for the given {@link module:engine/view/document~Document}.
+ *
+ * @param {module:engine/view/document~Document} viewDocument
+ * @return {Function}
+ */
+export function captionElementCreator( viewDocument ) {
+	return () => {
+		const editable = new ViewEditableElement( 'figcaption', { contenteditable: true } );
+		editable.document = viewDocument;
+		editable.setCustomProperty( captionSymbol, true );
+
+		editable.on( 'change:isFocused', ( evt, property, is ) => {
+			if ( is ) {
+				editable.addClass( 'focused' );
+			} else {
+				editable.removeClass( 'focused' );
+			}
+		} );
+
+		return editable;
+	};
+}
+
+/**
+ * Returns `true` if given view element is image's caption editable.
+ *
+ * @param {module:engine/view/element~Element} viewElement
+ * @return {Boolean}
+ */
+export function isCaption( viewElement ) {
+	return !!viewElement.getCustomProperty( captionSymbol );
+}
+
+/**
+ * Returns caption's model element from given image element. Returns `null` if no caption is found.
+ *
+ * @param {module:engine/model/element~Element} imageModelElement
+ * @return {module:engine/model/element~Element|null}
+ */
+export function getCaptionFromImage( imageModelElement ) {
+	for ( let node of imageModelElement.getChildren() ) {
+		if ( node instanceof ModelElement && node.name == 'caption' ) {
+			return node;
+		}
+	}
+
+	return null;
+}

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

@@ -4,11 +4,11 @@
  */
 
 /**
- * @module image/imagestyle/imagestyle
+ * @module image/imagestyle
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageStyleEngine from './imagestyleengine';
+import ImageStyleEngine from './imagestyle/imagestyleengine';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 /**

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

@@ -7,7 +7,7 @@
  * @module image/imagestyle/converters
  */
 
-import { isImage } from '../utils';
+import { isImage } from '../image/utils';
 
 /**
  * Returns converter for the `imageStyle` attribute. It can be used for adding, changing and removing the attribute.

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

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command/command';
-import { isImage } from '../utils';
+import { isImage } from '../image/utils';
 
 /**
  * The image style command. It is used to apply different image styles.

+ 1 - 1
packages/ckeditor5-image/src/imagestyle/imagestyleengine.js

@@ -9,7 +9,7 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ImageStyleCommand from './imagestylecommand';
-import ImageEngine from '../imageengine';
+import ImageEngine from '../image/imageengine';
 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';

+ 23 - 23
packages/ckeditor5-image/src/imagealternatetext/imagealternatetext.js → packages/ckeditor5-image/src/imagetextalternative.js

@@ -4,32 +4,32 @@
  */
 
 /**
- * @module image/imagealternatetext/imagealternatetext
+ * @module image/imagetextalternative
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import ImageAlternateTextEngine from './imagealternatetextengine';
+import ImageTextAlternativeEngine from './imagetextalternative/imagetextalternativeengine';
 import escPressHandler from '@ckeditor/ckeditor5-ui/src/bindings/escpresshandler';
 import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
-import ImageToolbar from '../imagetoolbar';
-import AlternateTextFormView from './ui/alternatetextformview';
-import ImageBalloonPanel from '../ui/imageballoonpanelview';
+import ImageToolbar from './imagetoolbar';
+import TextAlternativeFormView from './imagetextalternative/ui/textalternativeformview';
+import ImageBalloonPanel from './image/ui/imageballoonpanelview';
 
-import alternateTextIcon from '@ckeditor/ckeditor5-core/theme/icons/input.svg';
-import '../../theme/imagealternatetext/theme.scss';
+import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
+import '../theme/imagetextalternative/theme.scss';
 
 /**
- * The image alternate text plugin.
+ * The image text alternative plugin.
  *
  * @extends module:core/plugin~Plugin
  */
-export default class ImageAlternateText extends Plugin {
+export default class ImageTextAlternative extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageAlternateTextEngine ];
+		return [ ImageTextAlternativeEngine ];
 	}
 
 	/**
@@ -42,12 +42,12 @@ export default class ImageAlternateText extends Plugin {
 		const defaultImageToolbarConfig = this.editor.config.get( 'image.defaultToolbar' );
 
 		if ( defaultImageToolbarConfig ) {
-			defaultImageToolbarConfig.push( 'imageAlternateText' );
+			defaultImageToolbarConfig.push( 'imageTextAlternative' );
 		}
 
 		return this._createBalloonPanel().then( panel => {
 			/**
-			 * Balloon panel containing alternate text change form.
+			 * Balloon panel containing text alternative change form.
 			 *
 			 * @member {module:image/ui/imageballoonpanel~ImageBalloonPanelView} #baloonPanel
 			 */
@@ -56,29 +56,29 @@ export default class ImageAlternateText extends Plugin {
 			/**
 			 * Form containing textarea and buttons, used to change `alt` text value.
 			 *
-			 * @member {module:image/imagealternatetext/ui/imagealternatetextformview~AlternateTextFormView} #form
+			 * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form
 			 */
 			this.form = panel.content.get( 0 );
 		} );
 	}
 
 	/**
-	 * Creates button showing alternate text change balloon panel and registers it in
+	 * Creates button showing text alternative change balloon panel and registers it in
 	 * editor's {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
 	 *
 	 * @private
 	 */
 	_createButton() {
 		const editor = this.editor;
-		const command = editor.commands.get( 'imageAlternateText' );
+		const command = editor.commands.get( 'imageTextAlternative' );
 		const t = editor.t;
 
-		editor.ui.componentFactory.add( 'imageAlternateText', ( locale ) => {
+		editor.ui.componentFactory.add( 'imageTextAlternative', ( locale ) => {
 			const view = new ButtonView( locale );
 
 			view.set( {
-				label: t( 'Change alternate text' ),
-				icon: alternateTextIcon,
+				label: t( 'Change image text alternative' ),
+				icon: textAlternativeIcon,
 				tooltip: true
 			} );
 
@@ -94,20 +94,20 @@ export default class ImageAlternateText extends Plugin {
 	 * Creates balloon panel.
 	 *
 	 * @private
-	 * @return {Promise.<module:image/ui/imageballoonpanel~ImageBalloonPanelView>}
+	 * @return {Promise.<module:image/image/ui/imageballoonpanel~ImageBalloonPanelView>}
 	 */
 	_createBalloonPanel() {
 		const editor = this.editor;
 
 		const panel = new ImageBalloonPanel( editor );
-		const form = new AlternateTextFormView( editor.locale );
+		const form = new TextAlternativeFormView( editor.locale );
 
 		this.listenTo( form, 'submit', () => {
-			editor.execute( 'imageAlternateText', { newValue: form.lebeledInput.inputView.element.value } );
+			editor.execute( 'imageTextAlternative', { newValue: form.lebeledInput.inputView.element.value } );
 			this._hideBalloonPanel();
 		} );
 
-		// If image toolbar is present - hide it when alternate text balloon is visible.
+		// If image toolbar is present - hide it when text alternative balloon is visible.
 		const imageToolbar = editor.plugins.get( ImageToolbar );
 
 		if ( imageToolbar ) {
@@ -152,7 +152,7 @@ export default class ImageAlternateText extends Plugin {
 	 */
 	_showBalloonPanel() {
 		const editor = this.editor;
-		const command = editor.commands.get( 'imageAlternateText' );
+		const command = editor.commands.get( 'imageTextAlternative' );
 		this.form.lebeledInput.value = command.value || '';
 		this.balloonPanel.attach();
 		this.form.lebeledInput.select();

+ 4 - 4
packages/ckeditor5-image/src/imagealternatetext/imagealternatetextcommand.js → packages/ckeditor5-image/src/imagetextalternative/imagetextalternativecommand.js

@@ -4,18 +4,18 @@
  */
 
 /**
- * @module image/imagelaternatetext/imagealternatetextcommand
+ * @module image/imagelaternatetext/imagetextalternativecommand
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command/command';
-import { isImage } from '../utils';
+import { isImage } from '../image/utils';
 
 /**
- * The image alternate text command. It is used to change `alt` attribute on `image` elements.
+ * The image text alternative command. It is used to change `alt` attribute on `image` elements.
  *
  * @extends module:core/command/command~Command
  */
-export default class ImageAlternateTextCommand extends Command {
+export default class ImageTextAlternativeCommand extends Command {
 	/**
 	 * @inheritDoc
 	 */

+ 26 - 0
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeengine.js

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagetextalternative/imagetextalternativeengine
+ */
+
+import ImageTextAlternativeCommand from './imagetextalternativecommand';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+/**
+ * The ImageTextAlternativeEngine plugin.
+ * Registers `imageTextAlternative` command.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageTextAlternativeEngine extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this.editor.commands.set( 'imageTextAlternative', new ImageTextAlternativeCommand( this.editor ) );
+	}
+}

+ 6 - 6
packages/ckeditor5-image/src/imagealternatetext/ui/alternatetextformview.js → packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module image/imagealternatetext/ui/imagealternatetextformview
+ * @module image/imagetextalternative/ui/imagetextalternativeformview
  */
 
 import View from '@ckeditor/ckeditor5-ui/src/view';
@@ -15,11 +15,11 @@ import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
 import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 
 /**
- * AlternateTextFormView class.
+ * TextAlternativeFormView class.
  *
  * @extends module:ui/view~View
  */
-export default class AlternateTextFormView extends View {
+export default class TextAlternativeFormView extends View {
 	/**
 	 * @inheritDoc
 	 */
@@ -63,7 +63,7 @@ export default class AlternateTextFormView extends View {
 
 			attributes: {
 				class: [
-					'ck-alternate-text-form',
+					'cke-text-alternative-form',
 				]
 			},
 
@@ -74,7 +74,7 @@ export default class AlternateTextFormView extends View {
 
 					attributes: {
 						class: [
-							'ck-alternate-text-form__actions'
+							'cke-text-alternative-form__actions'
 						]
 					},
 
@@ -121,7 +121,7 @@ export default class AlternateTextFormView extends View {
 	_createLabeledInputView() {
 		const t = this.locale.t;
 		const labeledInput = new LabeledInputView( this.locale, InputTextView );
-		labeledInput.label = t( 'Alternate image text' );
+		labeledInput.label = t( 'Text alternative' );
 
 		return labeledInput;
 	}

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

@@ -10,8 +10,8 @@
 import Template from '@ckeditor/ckeditor5-ui/src/template';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
-import { isImageWidget } from './utils';
-import ImageBalloonPanel from './ui/imageballoonpanelview';
+import { isImageWidget } from './image/utils';
+import ImageBalloonPanel from './image/ui/imageballoonpanelview';
 
 /**
  * Image toolbar class. Creates image toolbar placed inside balloon panel that is showed when image widget is selected.
@@ -20,7 +20,7 @@ import ImageBalloonPanel from './ui/imageballoonpanelview';
  * Other plugins can add new components to the default toolbar configuration by pushing them to `image.defaultToolbar`
  * configuration. Default configuration is used when `image.toolbar` config is not present.
  *
- * @extends module:core/plugin~Plugin.
+ * @extends module:core/plugin~Plugin
  */
 export default class ImageToolbar extends Plugin {
 	/**

+ 28 - 5
packages/ckeditor5-image/src/widget/widget.js

@@ -13,6 +13,8 @@ import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobs
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
+import RootEditableElement from '@ckeditor/ckeditor5-engine/src/view/rooteditableelement';
 import { isWidget } from './utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
@@ -52,15 +54,20 @@ export default class Widget extends Plugin {
 	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
 	 */
 	_onMousedown( eventInfo, domEventData ) {
-		let widgetElement = domEventData.target;
 		const editor = this.editor;
 		const viewDocument = editor.editing.view;
+		let element = domEventData.target;
+
+		// Do nothing if inside nested editable.
+		if ( isInsideNestedEditable( element ) ) {
+			return;
+		}
 
 		// If target is not a widget element - check if one of the ancestors is.
-		if ( !isWidget( widgetElement ) ) {
-			widgetElement = widgetElement.findAncestor( element => isWidget( element ) );
+		if ( !isWidget( element ) ) {
+			element = element.findAncestor( isWidget );
 
-			if ( !widgetElement ) {
+			if ( !element ) {
 				return;
 			}
 		}
@@ -73,7 +80,7 @@ export default class Widget extends Plugin {
 		}
 
 		// Create model selection over widget.
-		const modelElement = editor.editing.mapper.toModelElement( widgetElement );
+		const modelElement = editor.editing.mapper.toModelElement( element );
 
 		editor.document.enqueueChanges( ( ) => {
 			this._setSelectionOverElement( modelElement );
@@ -235,3 +242,19 @@ function isArrowKeyCode( keyCode ) {
 function isDeleteKeyCode( keyCode ) {
 	return keyCode == keyCodes.delete || keyCode == keyCodes.backspace;
 }
+
+// Returns `true` when element is a nested editable or is placed inside one.
+//
+// @param {module:engine/view/element~Element}
+// @returns {Boolean}
+function isInsideNestedEditable( element ) {
+	while ( element ) {
+		if ( element instanceof ViewEditableElement && !( element instanceof RootEditableElement ) ) {
+			return true;
+		}
+
+		element = element.parent;
+	}
+
+	return false;
+}

+ 2 - 0
packages/ckeditor5-image/src/widget/widgetengine.js

@@ -34,6 +34,8 @@ export default class WidgetEngine extends Plugin {
 			}
 
 			const viewSelection = conversionApi.viewSelection;
+
+			// Check if widget was clicked or some sub-element.
 			const selectedElement = viewSelection.getSelectedElement();
 
 			if ( !selectedElement || !isWidget( selectedElement ) ) {

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

@@ -7,9 +7,9 @@
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Image from '../src/image';
-import ImageEngine from '../src/imageengine';
+import ImageEngine from '../src/image/imageengine';
 import Widget from '../src/widget/widget';
-import ImageAlternateText from '../src/imagealternatetext/imagealternatetext';
+import ImageTextAlternative from '../src/imagetextalternative';
 
 describe( 'Image', () => {
 	let editor;
@@ -38,7 +38,7 @@ describe( 'Image', () => {
 		expect( editor.plugins.get( Widget ) ).to.instanceOf( Widget );
 	} );
 
-	it( 'should load ImageAlternateText plugin', () => {
-		expect( editor.plugins.get( ImageAlternateText ) ).to.instanceOf( ImageAlternateText );
+	it( 'should load ImageTextAlternative plugin', () => {
+		expect( editor.plugins.get( ImageTextAlternative ) ).to.instanceOf( ImageTextAlternative );
 	} );
 } );

+ 15 - 31
packages/ckeditor5-image/tests/converters.js → packages/ckeditor5-image/tests/image/converters.js

@@ -3,14 +3,14 @@
  * For licensing, see LICENSE.md.
  */
 
-import { viewToModelImage, createImageAttributeConverter } from '../src/converters';
+import { viewToModelImage, createImageAttributeConverter } from '../../src/image/converters';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import { createImageViewElement } from '../src/imageengine';
-import { toImageWidget } from '../src/utils';
+import { createImageViewElement } from '../../src/image/imageengine';
+import { toImageWidget } from '../../src/image/utils';
 import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import { parse as parseView, getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-import { stringify as stringifyModel, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'Image converters', () => {
 	let editor, document, viewDocument;
@@ -51,17 +51,13 @@ describe( 'Image converters', () => {
 		} );
 
 		it( 'should convert view figure element', () => {
-			test(
-				'<figure class="image"><img src="foo.png" alt="bar baz"></img></figure>',
-				'<image alt="bar baz" src="foo.png"></image>'
-			);
+			editor.setData( '<figure class="image"><img src="foo.png" alt="bar baz"></img></figure>' );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image alt="bar baz" src="foo.png"></image>' );
 		} );
 
 		it( 'should convert without alt', () => {
-			test(
-				'<figure class="image"><img src="foo.png"></img></figure>',
-				'<image src="foo.png"></image>'
-			);
+			editor.setData( '<figure class="image"><img src="foo.png"></img></figure>' );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
 		} );
 
 		it( 'should not convert if figure element is already consumed', () => {
@@ -71,33 +67,21 @@ describe( 'Image converters', () => {
 				data.output = new ModelElement( 'not-image' );
 			}, { priority: 'high' } );
 
-			test(
-				'<figure class="image"><img src="foo.png" alt="bar baz"></img></figure>',
-				'<not-image></not-image>'
-			);
+			editor.setData( '<figure class="image"><img src="foo.png" alt="bar baz"></img></figure>' );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<not-image></not-image>' );
 		} );
 
 		it( 'should not convert image if schema disallows it', () => {
 			schema.disallow( { name: 'image', attributes: [ 'alt', 'src' ], inside: '$root' } );
-			const element = parseView( '<figure class="image"><img src="foo.png"></img></figure>' );
-			const model = dispatcher.convert( element );
 
-			expect( stringifyModel( model ) ).to.equal( '' );
+			editor.setData( '<figure class="image"><img src="foo.png"></img></figure>' );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
 		} );
 
 		it( 'should not convert image if there is no img element', () => {
-			const element = parseView( '<figure class="image"></figure>' );
-			const model = dispatcher.convert( element );
-
-			expect( stringifyModel( model ) ).to.equal( '' );
+			editor.setData( '<figure class="image"></figure>' );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
 		} );
-
-		function test( viewString, modelString ) {
-			const element = parseView( viewString );
-			const model = dispatcher.convert( element );
-
-			expect( stringifyModel( model ) ).to.equal( modelString );
-		}
 	} );
 
 	describe( 'modelToViewAttributeConverter', () => {

+ 11 - 2
packages/ckeditor5-image/tests/imageengine.js → packages/ckeditor5-image/tests/image/imageengine.js

@@ -4,12 +4,12 @@
  */
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageEngine from '../src/imageengine';
+import ImageEngine from '../../src/image/imageengine';
 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 buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
 import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import { isImageWidget } from '../src/utils';
+import { isImageWidget } from '../../src/image/utils';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 
 describe( 'ImageEngine', () => {
@@ -132,6 +132,15 @@ describe( 'ImageEngine', () => {
 				expect( getModelData( document, { withoutSelection: true } ) )
 					.to.equal( '' );
 			} );
+
+			it( 'should dispatch conversion for nested elements', () => {
+				const conversionSpy = sinon.spy();
+				editor.data.viewToModel.on( 'element:figcaption', conversionSpy );
+
+				editor.setData( '<figure class="image"><img src="foo.png" alt="alt text" /><figcaption></figcaption></figure>' );
+
+				sinon.assert.calledOnce( conversionSpy );
+			} );
 		} );
 	} );
 

+ 2 - 2
packages/ckeditor5-image/tests/ui/imageballoonpanelview.js → packages/ckeditor5-image/tests/image/ui/imageballoonpanelview.js

@@ -7,9 +7,9 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import ImageBalloonPanel from '../../src/ui/imageballoonpanelview';
+import ImageBalloonPanel from '../../../src/image/ui/imageballoonpanelview';
 import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/balloonpanel/balloonpanelview';
-import ImageEngine from '../../src/imageengine';
+import ImageEngine from '../../../src/image/imageengine';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'ImageBalloonPanel', () => {

+ 2 - 2
packages/ckeditor5-image/tests/utils.js → packages/ckeditor5-image/tests/image/utils.js

@@ -5,8 +5,8 @@
 
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import { toImageWidget, isImageWidget, isImage } from '../src/utils';
-import { isWidget, getFakeSelectionLabel } from '../src/widget/utils';
+import { toImageWidget, isImageWidget, isImage } from '../../src/image/utils';
+import { isWidget, getFakeSelectionLabel } from '../../src/widget/utils';
 
 describe( 'image widget utils', () => {
 	let element, image;

+ 34 - 0
packages/ckeditor5-image/tests/imagecaption.js

@@ -0,0 +1,34 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global window */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import ImageCaption from '../src/imagecaption';
+import ImageCaptionEngine from '../src/imagecaption/imagecaptionengine';
+
+describe( 'ImageCaption', () => {
+	let editor;
+
+	beforeEach( () => {
+		const editorElement = window.document.createElement( 'div' );
+		window.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor.create( editorElement, {
+			plugins: [ ImageCaption ]
+		} )
+		.then( newEditor => {
+			editor = newEditor;
+		} );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( ImageCaption ) ).to.instanceOf( ImageCaption );
+	} );
+
+	it( 'should load ImageCaptionEngine plugin', () => {
+		expect( editor.plugins.get( ImageCaptionEngine ) ).to.instanceOf( ImageCaptionEngine );
+	} );
+} );

+ 288 - 0
packages/ckeditor5-image/tests/imagecaption/imagecaptionengine.js

@@ -0,0 +1,288 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import ViewAttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeelement';
+import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
+import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
+import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
+import ImageCaptionEngine from '../../src/imagecaption/imagecaptionengine';
+import ImageEngine from '../../src/image/imageengine';
+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 buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
+import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
+
+describe( 'ImageCaptionEngine', () => {
+	let editor, document, viewDocument;
+
+	beforeEach( () => {
+		return VirtualTestEditor.create( {
+			plugins: [ ImageCaptionEngine, ImageEngine ]
+		} )
+			.then( newEditor => {
+				editor = newEditor;
+				document = editor.document;
+				viewDocument = editor.editing.view;
+				document.schema.registerItem( 'widget' );
+				document.schema.allow( { name: 'widget', inside: '$root' } );
+				document.schema.allow( { name: 'caption', inside: 'widget' } );
+				document.schema.allow( { name: '$inline', inside: 'widget' } );
+
+				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'widget' ).toElement( 'widget' );
+				buildModelConverter().for( editor.data.modelToView, editor.editing.modelToView ).fromElement( 'widget' ).toElement( 'widget' );
+			} );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( ImageCaptionEngine ) ).to.be.instanceOf( ImageCaptionEngine );
+	} );
+
+	it( 'should set proper schema rules', () => {
+		expect( document.schema.check( { name: 'caption', iniside: 'image' } ) ).to.be.true;
+		expect( document.schema.check( { name: '$inline', inside: 'caption' } ) ).to.be.true;
+		expect( document.schema.limits.has( 'caption' ) );
+	} );
+
+	describe( 'data pipeline', () => {
+		describe( 'view to model', () => {
+			it( 'should convert figcaption inside image figure', () => {
+				editor.setData( '<figure class="image"><img src="foo.png"/><figcaption>foo bar</figcaption></figure>' );
+
+				expect( getModelData( document, { withoutSelection: true } ) )
+					.to.equal( '<image src="foo.png"><caption>foo bar</caption></image>' );
+			} );
+
+			it( 'should add empty caption if there is no figcaption', () => {
+				editor.setData( '<figure class="image"><img src="foo.png"/></figure>' );
+
+				expect( getModelData( document, { withoutSelection: true } ) )
+					.to.equal( '<image src="foo.png"><caption></caption></image>' );
+			} );
+
+			it( 'should not convert figcaption inside other elements than image', () => {
+				editor.setData( '<widget><figcaption>foobar</figcaption></widget>' );
+
+				expect( getModelData( document, { withoutSelection: true } ) )
+					.to.equal( '<widget>foobar</widget>' );
+			} );
+		} );
+
+		describe( 'model to view', () => {
+			it( 'should convert caption element to figcaption', () => {
+				setModelData( document, '<image src="img.png"><caption>Foo bar baz.</caption></image>' );
+
+				expect( editor.getData() ).to.equal( '<figure class="image"><img src="img.png"><figcaption>Foo bar baz.</figcaption></figure>' );
+			} );
+
+			it( 'should not convert caption if it\'s empty', () => {
+				setModelData( document, '<image src="img.png"><caption></caption></image>' );
+
+				expect( editor.getData() ).to.equal( '<figure class="image"><img src="img.png"></figure>' );
+			} );
+
+			it( 'should not convert caption from other elements', () => {
+				setModelData( document, '<widget>foo bar<caption></caption></widget>' );
+				expect( editor.getData() ).to.equal( '<widget>foo bar</widget>' );
+			} );
+		} );
+	} );
+
+	describe( 'editing pipeline', () => {
+		describe( 'model to view', () => {
+			it( 'should convert caption element to figcaption contenteditable', () => {
+				setModelData( document, '<image src="img.png"><caption>Foo bar baz.</caption></image>' );
+
+				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+					'<figure class="image ck-widget" contenteditable="false">' +
+						'<img src="img.png"></img>' +
+						'<figcaption contenteditable="true">Foo bar baz.</figcaption>' +
+					'</figure>'
+				);
+			} );
+
+			it( 'should not convert caption if it\'s empty', () => {
+				setModelData( document, '<image src="img.png"><caption></caption></image>' );
+
+				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+					'<figure class="image ck-widget" contenteditable="false"><img src="img.png"></img></figure>'
+				);
+			} );
+
+			it( 'should not convert caption from other elements', () => {
+				setModelData( document, '<widget>foo bar<caption></caption></widget>' );
+				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal( '<widget>foo bar</widget>' );
+			} );
+
+			it( 'should not convert when element is already consumed', () => {
+				editor.editing.modelToView.on(
+					'insert:caption',
+					( evt, data, consumable, conversionApi ) => {
+						consumable.consume( data.item, 'insert' );
+
+						const imageFigure = conversionApi.mapper.toViewElement( data.range.start.parent );
+						const viewElement = new ViewAttributeElement( 'span' );
+
+						const viewPosition = ViewPosition.createAt( imageFigure, 'end' );
+						conversionApi.mapper.bindElements( data.item, viewElement );
+						viewWriter.insert( viewPosition, viewElement );
+					},
+					{ priority: 'high' }
+				);
+
+				setModelData( document, '<image src="img.png"><caption>Foo bar baz.</caption></image>' );
+
+				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+					'<figure class="image ck-widget" contenteditable="false"><img src="img.png"></img><span></span>Foo bar baz.</figure>'
+				);
+			} );
+		} );
+	} );
+
+	describe( 'inserting image to document', () => {
+		it( 'should add caption element if image does not have it', () => {
+			const image = new ModelElement( 'image', { src: '', alt: '' } );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.insert( new ModelPosition( document.getRoot(), [ 0 ] ), image );
+			} );
+
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+				'<image alt="" src=""><caption></caption></image>'
+			);
+		} );
+
+		it( 'should not add caption element if image does not have it', () => {
+			const caption = new ModelElement( 'caption', null, 'foo bar' );
+			const image = new ModelElement( 'image', { src: '', alt: '' }, caption );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.insert( new ModelPosition( document.getRoot(), [ 0 ] ), image );
+			} );
+
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+				'<image alt="" src=""><caption>foo bar</caption></image>'
+			);
+		} );
+
+		it( 'should do nothing for other changes than insert', () => {
+			setModelData( document, '<image src=""><caption>foo bar</caption></image>' );
+			const image = document.getRoot().getChild( 0 );
+			const batch = document.batch();
+
+			document.enqueueChanges( () => {
+				batch.setAttribute( image, 'alt', 'alt text' );
+			} );
+
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+				'<image alt="alt text" src=""><caption>foo bar</caption></image>'
+			);
+		} );
+	} );
+
+	describe( 'editing view', () => {
+		it( 'image should have empty figcaption element when is selected', () => {
+			setModelData( document, '[<image src=""><caption></caption></image>]' );
+
+			expect( getViewData( viewDocument ) ).to.equal(
+				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+					'<img src=""></img>' +
+					'<figcaption contenteditable="true"></figcaption>' +
+				'</figure>]'
+			);
+		} );
+
+		it( 'image should not have empty figcaption element when is not selected', () => {
+			setModelData( document, '[]<image src=""><caption></caption></image>' );
+
+			expect( getViewData( viewDocument ) ).to.equal(
+				'[]<figure class="image ck-widget" contenteditable="false">' +
+					'<img src=""></img>' +
+				'</figure>'
+			);
+		} );
+
+		it( 'should not add additional figcaption if one is already present', () => {
+			setModelData( document, '[<image src=""><caption>foo bar</caption></image>]' );
+
+			expect( getViewData( viewDocument ) ).to.equal(
+				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+					'<img src=""></img>' +
+					'<figcaption contenteditable="true">foo bar</figcaption>' +
+				'</figure>]'
+			);
+		} );
+
+		it( 'should remove figcaption when caption is empty and image is no longer selected', () => {
+			setModelData( document, '[<image src=""><caption></caption></image>]' );
+
+			document.enqueueChanges( () => {
+				document.selection.removeAllRanges();
+			} );
+
+			expect( getViewData( viewDocument ) ).to.equal(
+				'[]<figure class="image ck-widget" contenteditable="false">' +
+					'<img src=""></img>' +
+				'</figure>'
+			);
+		} );
+
+		it( 'should not remove figcaption when selection is inside it even when it is empty', () => {
+			setModelData( document, '<image src=""><caption>[foo bar]</caption></image>' );
+
+			document.enqueueChanges( () => {
+				document.batch().remove( document.selection.getFirstRange() );
+			} );
+
+			expect( getViewData( viewDocument ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false">' +
+					'<img src=""></img>' +
+					'<figcaption contenteditable="true">[]</figcaption>' +
+				'</figure>'
+			);
+		} );
+
+		it( 'should not remove figcaption when selection is moved from it to its image', () => {
+			setModelData( document, '<image src=""><caption>[foo bar]</caption></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			document.enqueueChanges( () => {
+				document.batch().remove( document.selection.getFirstRange() );
+				document.selection.setRanges( [ ModelRange.createOn( image ) ] );
+			} );
+
+			expect( getViewData( viewDocument ) ).to.equal(
+				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+					'<img src=""></img>' +
+					'<figcaption contenteditable="true"></figcaption>' +
+				'</figure>]'
+			);
+		} );
+
+		it( 'should not remove figcaption when selection is moved from it to other image', () => {
+			setModelData( document, '<image src=""><caption>[foo bar]</caption></image><image src=""><caption></caption></image>' );
+			const image = document.getRoot().getChild( 1 );
+
+			document.enqueueChanges( () => {
+				document.selection.setRanges( [ ModelRange.createOn( image ) ] );
+			} );
+
+			expect( getViewData( viewDocument ) ).to.equal(
+				'<figure class="image ck-widget" contenteditable="false">' +
+					'<img src=""></img>' +
+					'<figcaption contenteditable="true">foo bar</figcaption>' +
+				'</figure>' +
+				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+					'<img src=""></img>' +
+					'<figcaption contenteditable="true"></figcaption>' +
+				'</figure>]'
+			);
+		} );
+	} );
+} );

+ 68 - 0
packages/ckeditor5-image/tests/imagecaption/utils.js

@@ -0,0 +1,68 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
+import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
+import { captionElementCreator, isCaption, getCaptionFromImage } from '../../src/imagecaption/utils';
+import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+
+describe( 'image captioning utils', () => {
+	let element, document;
+
+	beforeEach( () => {
+		document = new ViewDocument();
+		const creator = captionElementCreator( document );
+		element = creator();
+	} );
+
+	describe( 'editableCaptionCreator', () => {
+		it( 'should create figcatpion editable element', () => {
+			expect( element ).to.be.instanceOf( ViewEditableElement );
+			expect( element.name ).to.equal( 'figcaption' );
+			expect( isCaption( element ) ).to.be.true;
+		} );
+
+		it( 'should be created in context of proper document', () => {
+			expect( element.document ).to.equal( document );
+		} );
+
+		it( 'should add proper class when element is focused', () => {
+			element.isFocused = true;
+			expect( element.hasClass( 'focused' ) ).to.be.true;
+
+			element.isFocused = false;
+			expect( element.hasClass( 'focused' ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'isCaptionEditable', () => {
+		it( 'should return true for elements created with creator', () => {
+			expect( isCaption( element ) ).to.be.true;
+		} );
+
+		it( 'should return false for other elements', () => {
+			const editable = new ViewEditableElement( 'figcaption', { contenteditable: true } ) ;
+			editable.document = document;
+
+			expect( isCaption( editable ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'getCaptionFromImage', () => {
+		it( 'should return caption element from image element', () => {
+			const dummy = new ModelElement( 'dummy' );
+			const caption = new ModelElement( 'caption' );
+			const image = new ModelElement( 'image', null, [ dummy, caption ] );
+
+			expect( getCaptionFromImage( image ) ).to.equal( caption );
+		} );
+
+		it( 'should return null when caption element is not present', () => {
+			const image = new ModelElement( 'image' );
+
+			expect( getCaptionFromImage( image ) ).to.be.null;
+		} );
+	} );
+} );

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

@@ -4,9 +4,9 @@
  */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-import ImageToolbar from '../../src/imagetoolbar';
-import ImageStyle from '../../src/imagestyle/imagestyle';
-import ImageStyleEngine from '../../src/imagestyle/imagestyleengine';
+import ImageToolbar from '../src/imagetoolbar';
+import ImageStyle from '../src/imagestyle';
+import ImageStyleEngine from '../src/imagestyle/imagestyleengine';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 

+ 1 - 1
packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js

@@ -5,7 +5,7 @@
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import ImageStyleEngine from '../../src/imagestyle/imagestyleengine';
-import ImageEngine from '../../src/imageengine';
+import ImageEngine from '../../src/image/imageengine';
 import ImageStyleCommand from '../../src/imagestyle/imagestylecommand';
 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';

+ 24 - 24
packages/ckeditor5-image/tests/imagealternatetext/imagealternatetext.js → packages/ckeditor5-image/tests/imagetextalternative.js

@@ -4,10 +4,10 @@
  */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
-import Image from '../../src/image';
-import ImageToolbar from '../../src/imagetoolbar';
-import ImageAlternateText from '../../src/imagealternatetext/imagealternatetext';
-import ImageAlternateTextEngine from '../../src/imagealternatetext/imagealternatetextengine';
+import Image from '../src/image';
+import ImageToolbar from '../src/imagetoolbar';
+import ImageTextAlternative from '../src/imagetextalternative';
+import ImageTextAlternativeEngine from '../src/imagetextalternative/imagetextalternativeengine';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -15,7 +15,7 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 /* global Event */
 
-describe( 'ImageAlternateText', () => {
+describe( 'ImageTextAlternative', () => {
 	let editor, plugin, command, balloonPanel, form;
 
 	beforeEach( () => {
@@ -23,13 +23,13 @@ describe( 'ImageAlternateText', () => {
 		global.document.body.appendChild( editorElement );
 
 		return ClassicTestEditor.create( editorElement, {
-			plugins: [ ImageAlternateText, Image ]
+			plugins: [ ImageTextAlternative, Image ]
 		} )
 		.then( newEditor => {
 			editor = newEditor;
 			newEditor.editing.view.attachDomRoot( editorElement );
-			plugin = editor.plugins.get( ImageAlternateText );
-			command = editor.commands.get( 'imageAlternateText' );
+			plugin = editor.plugins.get( ImageTextAlternative );
+			command = editor.commands.get( 'imageTextAlternative' );
 			balloonPanel = plugin.balloonPanel;
 			form = plugin.form;
 		} );
@@ -40,18 +40,18 @@ describe( 'ImageAlternateText', () => {
 	} );
 
 	it( 'should be loaded', () => {
-		expect( plugin ).to.be.instanceOf( ImageAlternateText );
+		expect( plugin ).to.be.instanceOf( ImageTextAlternative );
 	} );
 
-	it( 'should load ImageAlternateTextEngine plugin', () => {
-		expect( editor.plugins.get( ImageAlternateTextEngine ) ).to.be.instanceOf( ImageAlternateTextEngine );
+	it( 'should load ImageTextAlternativeEngine plugin', () => {
+		expect( editor.plugins.get( ImageTextAlternativeEngine ) ).to.be.instanceOf( ImageTextAlternativeEngine );
 	} );
 
 	describe( 'toolbar button', () => {
 		let button;
 
 		beforeEach( () => {
-			button = editor.ui.componentFactory.create( 'imageAlternateText' );
+			button = editor.ui.componentFactory.create( 'imageTextAlternative' );
 		} );
 
 		it( 'should be registered in component factory', () => {
@@ -101,10 +101,10 @@ describe( 'ImageAlternateText', () => {
 			global.document.body.appendChild( editorElement );
 
 			return ClassicTestEditor.create( editorElement, {
-				plugins: [ ImageAlternateText, ImageToolbar ]
+				plugins: [ ImageTextAlternative, ImageToolbar ]
 			} )
 			.then( newEditor => {
-				expect( newEditor.config.get( 'image.defaultToolbar' ) ).to.eql( [ 'imageAlternateText' ] );
+				expect( newEditor.config.get( 'image.defaultToolbar' ) ).to.eql( [ 'imageTextAlternative' ] );
 
 				newEditor.destroy();
 			} );
@@ -117,7 +117,7 @@ describe( 'ImageAlternateText', () => {
 			form.fire( 'submit' );
 
 			sinon.assert.calledOnce( spy );
-			sinon.assert.calledWithExactly( spy, 'imageAlternateText', { newValue: form.lebeledInput.inputView.element.value } );
+			sinon.assert.calledWithExactly( spy, 'imageTextAlternative', { newValue: form.lebeledInput.inputView.element.value } );
 		} );
 
 		it( 'should detach panel on cancel', () => {
@@ -132,14 +132,14 @@ describe( 'ImageAlternateText', () => {
 			global.document.body.appendChild( editorElement );
 
 			return ClassicTestEditor.create( editorElement, {
-				plugins: [ ImageAlternateText, Image, ImageToolbar ],
+				plugins: [ ImageTextAlternative, Image, ImageToolbar ],
 				image: {
-					toolbar: [ 'imageAlternateText' ]
+					toolbar: [ 'imageTextAlternative' ]
 				}
 			} )
 			.then( newEditor => {
 				newEditor.editing.view.attachDomRoot( editorElement );
-				const plugin = newEditor.plugins.get( ImageAlternateText );
+				const plugin = newEditor.plugins.get( ImageTextAlternative );
 				const toolbarPlugin = newEditor.plugins.get( ImageToolbar );
 				const form = plugin.form;
 
@@ -197,17 +197,17 @@ describe( 'ImageAlternateText', () => {
 			global.document.body.appendChild( editorElement );
 
 			return ClassicTestEditor.create( editorElement, {
-				plugins: [ ImageAlternateText, Image, ImageToolbar ],
+				plugins: [ ImageTextAlternative, Image, ImageToolbar ],
 				image: {
-					toolbar: [ 'imageAlternateText' ]
+					toolbar: [ 'imageTextAlternative' ]
 				}
 			} )
 			.then( newEditor => {
 				editor = newEditor;
 				editor.editing.view.attachDomRoot( editorElement );
-				button = newEditor.ui.componentFactory.create( 'imageAlternateText' );
+				button = newEditor.ui.componentFactory.create( 'imageTextAlternative' );
 				imageToolbarPlugin = newEditor.plugins.get( ImageToolbar );
-				plugin = editor.plugins.get( ImageAlternateText );
+				plugin = editor.plugins.get( ImageTextAlternative );
 			} );
 		} );
 
@@ -221,7 +221,7 @@ describe( 'ImageAlternateText', () => {
 			sinon.assert.calledOnce( spy );
 		} );
 
-		it( 'ImageToolbar should not show when alternate text panel is visible', () => {
+		it( 'ImageToolbar should not show when text alternative panel is visible', () => {
 			setData( editor.document, '[<image src="" alt="foo bar"></image>]' );
 			button.fire( 'execute' );
 			const spy = sinon.spy( imageToolbarPlugin, 'show' );
@@ -230,7 +230,7 @@ describe( 'ImageAlternateText', () => {
 			sinon.assert.notCalled( spy );
 		} );
 
-		it( 'ImageToolbar should show when alternate text panel is not visible', () => {
+		it( 'ImageToolbar should show when text alternative panel is not visible', () => {
 			setData( editor.document, '[<image src="" alt="foo bar"></image>]' );
 			button.fire( 'execute' );
 			const spy = sinon.spy( imageToolbarPlugin, 'show' );

+ 3 - 3
packages/ckeditor5-image/tests/imagealternatetext/imagealternatetextcommand.js → packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativecommand.js

@@ -4,17 +4,17 @@
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import ImageAlternateTextCommand from '../../src/imagealternatetext/imagealternatetextcommand';
+import ImageTextAlternativeCommand from '../../src/imagetextalternative/imagetextalternativecommand';
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-describe( 'ImageAlternateTextCommand', () => {
+describe( 'ImageTextAlternativeCommand', () => {
 	let document, command;
 
 	beforeEach( () => {
 		return ModelTestEditor.create()
 			.then( newEditor => {
 				document = newEditor.document;
-				command = new ImageAlternateTextCommand( newEditor );
+				command = new ImageTextAlternativeCommand( newEditor );
 
 				document.schema.registerItem( 'p', '$block' );
 

+ 5 - 5
packages/ckeditor5-image/tests/imagealternatetext/imagelaternatetextengine.js → packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeengine.js

@@ -4,14 +4,14 @@
  */
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageAlternateTextEngine from '../../src/imagealternatetext/imagealternatetextengine';
-import ImageAlternateTextCommand from '../../src/imagealternatetext/imagealternatetextcommand';
+import ImageTextAlternativeEngine from '../../src/imagetextalternative/imagetextalternativeengine';
+import ImageTextAlternativeCommand from '../../src/imagetextalternative/imagetextalternativecommand';
 
-describe( 'ImageAlternateTextEngine', () => {
+describe( 'ImageTextAlternativeEngine', () => {
 	let editor;
 	beforeEach( () => {
 		return VirtualTestEditor.create( {
-			plugins: [ ImageAlternateTextEngine ]
+			plugins: [ ImageTextAlternativeEngine ]
 		} )
 		.then( newEditor => {
 			editor = newEditor;
@@ -19,6 +19,6 @@ describe( 'ImageAlternateTextEngine', () => {
 	} );
 
 	it( 'should register ImageAlteranteTextCommand', () => {
-		expect( editor.commands.get( 'imageAlternateText' ) ).to.be.instanceOf( ImageAlternateTextCommand );
+		expect( editor.commands.get( 'imageTextAlternative' ) ).to.be.instanceOf( ImageTextAlternativeCommand );
 	} );
 } );

+ 4 - 4
packages/ckeditor5-image/tests/imagealternatetext/ui/alternatetextformview.js → packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js

@@ -5,21 +5,21 @@
 
 /* global Event */
 
-import AlternateTextFormView from '../../../src/imagealternatetext/ui/alternatetextformview';
+import TextAlternativeFormView from '../../../src/imagetextalternative/ui/textalternativeformview';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
-describe( 'AlternateTextFormView', () => {
+describe( 'TextAlternativeFormView', () => {
 	let view;
 
 	beforeEach( () => {
-		view = new AlternateTextFormView( { t: () => {} } );
+		view = new TextAlternativeFormView( { t: () => {} } );
 
 		view.init();
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'should create element from template', () => {
-			expect( view.element.classList.contains( 'ck-alternate-text-form' ) ).to.be.true;
+			expect( view.element.classList.contains( 'cke-text-alternative-form' ) ).to.be.true;
 		} );
 
 		it( 'should create child views', () => {

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

@@ -7,7 +7,7 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
 import ImageToolbar from '../src/imagetoolbar';
 import Image from '../src/image';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import ImageBalloonPanel from '../src/ui/imageballoonpanelview';
+import ImageBalloonPanel from '../src/image/ui/imageballoonpanelview';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';

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

@@ -1,10 +0,0 @@
-## Image alternate text
-
-* Click on image - balloon toolbar with alternate text change button should appear.
-* Click on button in toolbar:
-  * balloon toolbar should hide, 
-  * alternate image text balloon panel should be visible
-  * there should be `CKEditor logo` text selected inside textarea.
-* Change the text and press `Ok`. Check if alternate text is changed by opening balloon panel again.
-* Change the text and press `Cancel`. Check if alternate text is NOT changed by opening balloon panel again.
-

+ 11 - 0
packages/ckeditor5-image/tests/manual/caption.html

@@ -0,0 +1,11 @@
+<div id="editor">
+	<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">
+		<img src="logo.png" />
+		<figcaption>CKEditor logo - caption</figcaption>
+	</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. 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">
+		<img src="logo.png"  />
+	</figure>
+</div>

+ 35 - 0
packages/ckeditor5-image/tests/manual/caption.js

@@ -0,0 +1,35 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, console, window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Heading from '@ckeditor/ckeditor5-heading/src/heading';
+import Image from '../../src/image';
+import ImageCaption from '../../src/imagecaption';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import ImageToolbar from '../../src/imagetoolbar';
+import ImageStyle from '../../src/imagestyle';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+import List from '@ckeditor/ckeditor5-list/src/list';
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	plugins: [
+		Enter, Typing, Paragraph, Heading, Image, ImageToolbar,
+		Undo, Clipboard, ImageCaption, ImageStyle, Bold, Italic, Heading, List
+	],
+	toolbar: [ 'headings', 'undo', 'redo', 'bold', 'italic', 'bulletedList', 'numberedList' ]
+} )
+.then( editor => {
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );

+ 11 - 0
packages/ckeditor5-image/tests/manual/caption.md

@@ -0,0 +1,11 @@
+## Image captioning
+
+First image should have `CKEditor logo - caption` displayed.
+Second image shouldn't have any caption and caption editable area should not be visible.
+
+* Click on second image - editable area should be visible.
+* Click on editable area and add some text.
+* Put selection inside paragraph. Caption text and editable area should stay visible.
+* Click on editable area in first image.
+* Remove whole caption text.
+* Put selection inside paragraph. Caption editable area from first image should be hidden.

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

@@ -13,16 +13,16 @@ 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/imagestyle';
+import ImageStyle from '../../src/imagestyle';
 import ImageToolbar from '../../src/imagetoolbar';
 
 ClassicEditor.create( document.querySelector( '#editor' ), {
 	plugins: [ ImageToolbar, EnterPlugin, TypingPlugin, ParagraphPlugin, HeadingPlugin, ImagePlugin, UndoPlugin, ClipboardPlugin, ImageStyle ],
 	toolbar: [ 'headings', 'undo', 'redo' ]
 } )
-	.then( editor => {
-		window.editor = editor;
-	} )
-	.catch( err => {
-		console.error( err.stack );
-	} );
+.then( editor => {
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );

+ 0 - 0
packages/ckeditor5-image/tests/manual/alternatetext.html → packages/ckeditor5-image/tests/manual/textalternative.html


+ 0 - 0
packages/ckeditor5-image/tests/manual/alternatetext.js → packages/ckeditor5-image/tests/manual/textalternative.js


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

@@ -0,0 +1,10 @@
+## Image text alternative
+
+* Click on image - balloon toolbar with text alternative change button should appear.
+* Click on button in toolbar:
+  * balloon toolbar should hide,
+  * image text alternative balloon panel should be visible
+  * there should be `CKEditor logo` text selected inside textarea.
+* Change the text and press `Ok`. Check if text alternative is changed by opening balloon panel again.
+* Change the text and press `Cancel`. Check if text alternative is NOT changed by opening balloon panel again.
+

+ 23 - 0
packages/ckeditor5-image/tests/widget/widget.js

@@ -9,6 +9,7 @@ import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobs
 import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
 import { widgetize } from '../../src/widget/utils';
 import ViewContainer from '@ckeditor/ckeditor5-engine/src/view/containerelement';
+import ViewEditable from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import AttributeContainer from '@ckeditor/ckeditor5-engine/src/view/attributeelement';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -33,6 +34,9 @@ describe( 'Widget', () => {
 				doc.schema.registerItem( 'paragraph', '$block' );
 				doc.schema.registerItem( 'inline', '$inline' );
 				doc.schema.objects.add( 'inline' );
+				doc.schema.registerItem( 'nested' );
+				doc.schema.allow( { name: '$inline', inside: 'nested' } );
+				doc.schema.allow( { name: 'nested', inside: 'widget' } );
 
 				buildModelConverter().for( editor.editing.modelToView )
 					.fromElement( 'paragraph' )
@@ -50,6 +54,10 @@ describe( 'Widget', () => {
 				buildModelConverter().for( editor.editing.modelToView )
 					.fromElement( 'inline' )
 					.toElement( 'figure' );
+
+				buildModelConverter().for( editor.editing.modelToView )
+					.fromElement( 'nested' )
+					.toElement( () => new ViewEditable( 'figcaption', { contenteditable: true } ) );
 			} );
 	} );
 
@@ -90,6 +98,21 @@ describe( 'Widget', () => {
 		sinon.assert.calledOnce( domEventDataMock.preventDefault );
 	} );
 
+	it( 'should do nothing if clicked inside nested editable', () => {
+		setModelData( doc, '[]<widget><nested>foo bar</nested></widget>' );
+		const viewDiv = viewDocument.getRoot().getChild( 0 );
+		const viewFigcaption = viewDiv.getChild( 0 );
+
+		const domEventDataMock = {
+			target: viewFigcaption,
+			preventDefault: sinon.spy()
+		};
+
+		viewDocument.fire( 'mousedown', domEventDataMock );
+
+		sinon.assert.notCalled( domEventDataMock.preventDefault );
+	} );
+
 	it( 'should do nothing if clicked in non-widget element', () => {
 		setModelData( doc, '<paragraph>[]foo bar</paragraph><widget></widget>' );
 		const viewP = viewDocument.getRoot().getChild( 0 );

+ 20 - 0
packages/ckeditor5-image/tests/widget/widgetengine.js

@@ -9,6 +9,7 @@ import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/build
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import ViewContainer from '@ckeditor/ckeditor5-engine/src/view/containerelement';
+import ViewEditable from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import { widgetize, setFakeSelectionLabel } from '../../src/widget/utils';
 
 describe( 'WidgetEngine', () => {
@@ -23,6 +24,10 @@ describe( 'WidgetEngine', () => {
 				document = editor.document;
 				viewDocument = editor.editing.view;
 				document.schema.registerItem( 'widget', '$block' );
+				document.schema.registerItem( 'editable' );
+				document.schema.allow( { name: '$inline', inside: 'editable' } );
+				document.schema.allow( { name: 'editable', inside: 'widget' } );
+				document.schema.allow( { name: 'editable', inside: '$root' } );
 
 				buildModelConverter().for( editor.editing.modelToView )
 					.fromElement( 'widget' )
@@ -32,6 +37,10 @@ describe( 'WidgetEngine', () => {
 
 						return element;
 					} );
+
+				buildModelConverter().for( editor.editing.modelToView )
+					.fromElement( 'editable' )
+					.toElement( () => new ViewEditable( 'figcaption', { contenteditable: true } ) );
 			} );
 	} );
 
@@ -75,4 +84,15 @@ describe( 'WidgetEngine', () => {
 			'[]<div class="ck-widget" contenteditable="false">foo</div>'
 		);
 	} );
+
+	it( 'should do nothing when selection is placed in other editable', () => {
+		setModelData( document, '<widget><editable>foo bar</editable></widget><editable>[baz]</editable>' );
+
+		expect( getViewData( viewDocument ) ).to.equal(
+			'<div class="ck-widget" contenteditable="false">' +
+				'<figcaption contenteditable="true">foo bar</figcaption>' +
+			'</div>' +
+			'<figcaption contenteditable="true">{baz}</figcaption>'
+		);
+	} );
 } );

+ 27 - 0
packages/ckeditor5-image/theme/imagecaption/theme.scss

@@ -0,0 +1,27 @@
+// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+// For licensing, see LICENSE.md or http://ckeditor.com/license
+
+@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_colors.scss';
+@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_shadow.scss';
+@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_states.scss';
+
+.ck-widget.image {
+	figcaption {
+		background-color: ck-color( 'foreground' );
+		padding: 10px;
+
+		// The `:focus` styles is applied before `.focused` class inside editables.
+		// These styles show different border for a blink of an eye.
+		&:focus {
+			outline: none;
+			box-shadow: none;
+		}
+
+		&.focused {
+			@include ck-focus-ring( 'outline' );
+			@include ck-box-shadow( $ck-inner-shadow );
+			background-color: ck-color( 'background' );;
+		}
+
+	}
+}

+ 1 - 1
packages/ckeditor5-image/theme/imagealternatetext/theme.scss → packages/ckeditor5-image/theme/imagetextalternative/theme.scss

@@ -3,7 +3,7 @@
 
 @import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_spacing';
 
-.ck-alternate-text {
+.cke-text-alternative {
 	&-form {
 		padding: ck-spacing( 'large' );
 		overflow: hidden;