Browse Source

Merge branch 'master' into t/52

Aleksander Nowodzinski 8 years ago
parent
commit
e33c0230bd
67 changed files with 1573 additions and 287 deletions
  1. 26 0
      packages/ckeditor5-image/CHANGELOG.md
  2. 2 2
      packages/ckeditor5-image/lang/contexts.json
  3. 26 0
      packages/ckeditor5-image/lang/translations/ast.po
  4. 26 0
      packages/ckeditor5-image/lang/translations/de.po
  5. 23 0
      packages/ckeditor5-image/lang/translations/en.po
  6. 26 0
      packages/ckeditor5-image/lang/translations/fr.po
  7. 26 0
      packages/ckeditor5-image/lang/translations/hr.po
  8. 26 0
      packages/ckeditor5-image/lang/translations/hu.po
  9. 26 0
      packages/ckeditor5-image/lang/translations/it.po
  10. 26 0
      packages/ckeditor5-image/lang/translations/ku.po
  11. 26 0
      packages/ckeditor5-image/lang/translations/pt.po
  12. 26 0
      packages/ckeditor5-image/lang/translations/uk.po
  13. 26 0
      packages/ckeditor5-image/lang/translations/zh-cn.po
  14. 14 14
      packages/ckeditor5-image/package.json
  15. 4 4
      packages/ckeditor5-image/src/image.js
  16. 11 31
      packages/ckeditor5-image/src/converters.js
  17. 5 7
      packages/ckeditor5-image/src/imageengine.js
  18. 17 13
      packages/ckeditor5-image/src/ui/imageballoonpanelview.js
  19. 13 5
      packages/ckeditor5-image/src/utils.js
  20. 0 26
      packages/ckeditor5-image/src/imagealternatetext/imagealternatetextengine.js
  21. 26 0
      packages/ckeditor5-image/src/imagecaption.js
  22. 225 0
      packages/ckeditor5-image/src/imagecaption/imagecaptionengine.js
  23. 63 0
      packages/ckeditor5-image/src/imagecaption/utils.js
  24. 6 2
      packages/ckeditor5-image/src/imagestyle/imagestyle.js
  25. 1 1
      packages/ckeditor5-image/src/imagestyle/converters.js
  26. 1 1
      packages/ckeditor5-image/src/imagestyle/imagestylecommand.js
  27. 1 1
      packages/ckeditor5-image/src/imagestyle/imagestyleengine.js
  28. 27 23
      packages/ckeditor5-image/src/imagealternatetext/imagealternatetext.js
  29. 4 4
      packages/ckeditor5-image/src/imagealternatetext/imagealternatetextcommand.js
  30. 26 0
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeengine.js
  31. 6 6
      packages/ckeditor5-image/src/imagealternatetext/ui/alternatetextformview.js
  32. 4 6
      packages/ckeditor5-image/src/imagetoolbar.js
  33. 38 1
      packages/ckeditor5-image/src/widget/utils.js
  34. 28 5
      packages/ckeditor5-image/src/widget/widget.js
  35. 4 2
      packages/ckeditor5-image/src/widget/widgetengine.js
  36. 4 4
      packages/ckeditor5-image/tests/image.js
  37. 15 54
      packages/ckeditor5-image/tests/converters.js
  38. 11 2
      packages/ckeditor5-image/tests/imageengine.js
  39. 2 2
      packages/ckeditor5-image/tests/ui/imageballoonpanelview.js
  40. 22 5
      packages/ckeditor5-image/tests/utils.js
  41. 34 0
      packages/ckeditor5-image/tests/imagecaption.js
  42. 288 0
      packages/ckeditor5-image/tests/imagecaption/imagecaptionengine.js
  43. 68 0
      packages/ckeditor5-image/tests/imagecaption/utils.js
  44. 26 3
      packages/ckeditor5-image/tests/imagestyle/imagestyle.js
  45. 1 1
      packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js
  46. 47 24
      packages/ckeditor5-image/tests/imagealternatetext/imagealternatetext.js
  47. 3 3
      packages/ckeditor5-image/tests/imagealternatetext/imagealternatetextcommand.js
  48. 5 5
      packages/ckeditor5-image/tests/imagealternatetext/imagelaternatetextengine.js
  49. 4 4
      packages/ckeditor5-image/tests/imagealternatetext/ui/alternatetextformview.js
  50. 1 1
      packages/ckeditor5-image/tests/imagetoolbar.js
  51. 0 10
      packages/ckeditor5-image/tests/manual/alternatetext.md
  52. 11 0
      packages/ckeditor5-image/tests/manual/caption.html
  53. 35 0
      packages/ckeditor5-image/tests/manual/caption.js
  54. 11 0
      packages/ckeditor5-image/tests/manual/caption.md
  55. 3 3
      packages/ckeditor5-image/tests/manual/image.html
  56. 1 1
      packages/ckeditor5-image/tests/manual/imagestyle.html
  57. 7 7
      packages/ckeditor5-image/tests/manual/imagestyle.js
  58. BIN
      packages/ckeditor5-image/tests/manual/logo.png
  59. BIN
      packages/ckeditor5-image/tests/manual/sample.jpg
  60. 1 1
      packages/ckeditor5-image/tests/manual/alternatetext.html
  61. 0 0
      packages/ckeditor5-image/tests/manual/textalternative.js
  62. 10 0
      packages/ckeditor5-image/tests/manual/textalternative.md
  63. 40 1
      packages/ckeditor5-image/tests/widget/utils.js
  64. 23 0
      packages/ckeditor5-image/tests/widget/widget.js
  65. 37 1
      packages/ckeditor5-image/tests/widget/widgetengine.js
  66. 27 0
      packages/ckeditor5-image/theme/imagecaption/theme.scss
  67. 1 1
      packages/ckeditor5-image/theme/imagealternatetext/theme.scss

+ 26 - 0
packages/ckeditor5-image/CHANGELOG.md

@@ -0,0 +1,26 @@
+Changelog
+=========
+
+## [0.4.0](https://github.com/ckeditor/ckeditor5-image/compare/v0.3.0...v0.4.0) (2017-03-06)
+
+### Bug fixes
+
+* Moved focus tracking setup to `ImageBalloonPanelView#init()` method to prevent too early access to the view element. Closes [#42](https://github.com/ckeditor/ckeditor5/issues/42). ([985e509](https://github.com/ckeditor/ckeditor5-image/commit/985e509))
+* Used "low-vision" icon instead of "input" for text alternative button. Closes [#59](https://github.com/ckeditor/ckeditor5/issues/59). ([6edd823](https://github.com/ckeditor/ckeditor5-image/commit/6edd823))
+
+### Features
+
+* Added a separator between image styles and text alternative buttons in the image toolbar. Closes [#64](https://github.com/ckeditor/ckeditor5/issues/64). ([925a538](https://github.com/ckeditor/ckeditor5-image/commit/925a538))
+* Introduced image captions support. Closes [#28](https://github.com/ckeditor/ckeditor5/issues/28). ([6bb4069](https://github.com/ckeditor/ckeditor5-image/commit/6bb4069))
+
+### Other changes
+
+* Enhanced how selection label for widgets is defined. Closes [#9](https://github.com/ckeditor/ckeditor5/issues/9). ([5c1897d](https://github.com/ckeditor/ckeditor5-image/commit/5c1897d))
+* Renamed "Image alternate text" to "Image text alternative" all across the code. Improved directory structure to have most important features in the top level of `src/`. Closes [#37](https://github.com/ckeditor/ckeditor5/issues/37). ([e38675f](https://github.com/ckeditor/ckeditor5-image/commit/e38675f))
+* Uploaded translations. ([d619f1d](https://github.com/ckeditor/ckeditor5-image/commit/d619f1d))
+
+
+### BREAKING CHANGES
+
+* The `src/imagealternatetext/imagealternatetext` module is now `src/imagetextalternative`. All other related classes and directories were renamed too.
+* All the base image's util modules are now inside `src/image/`. The same applies to all other features. The main features are now directly in `src/`. Closes [#33](https://github.com/ckeditor/ckeditor5/issues/33). Closes [#26](https://github.com/ckeditor/ckeditor5/issues/26).

+ 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."
 }

+ 26 - 0
packages/ckeditor5-image/lang/translations/ast.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Asturian (https://www.transifex.com/ckeditor/teams/11143/ast/)\n"
+"Language: ast\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "complementu d'imaxen"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Imaxen llateral"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Imaxen a tamañu completu"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr ""
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr ""

+ 26 - 0
packages/ckeditor5-image/lang/translations/de.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: German (https://www.transifex.com/ckeditor/teams/11143/de/)\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "Bild-Steuerelement"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Seitenbild"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Bild in voller Größe"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Alternativ Text ändern"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Textalternative"

+ 23 - 0
packages/ckeditor5-image/lang/translations/en.po

@@ -0,0 +1,23 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "image widget"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Side image"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Full size image"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Change image text alternative"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Text alternative"

+ 26 - 0
packages/ckeditor5-image/lang/translations/fr.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: French (https://www.transifex.com/ckeditor/teams/11143/fr/)\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "objet image"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Image sur le côté"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Image taille réelle"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Changer le texte alternatif à l’image"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Texte alternatif"

+ 26 - 0
packages/ckeditor5-image/lang/translations/hr.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Croatian (https://www.transifex.com/ckeditor/teams/11143/hr/)\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "Slika widget"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Slika sa strane"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Slika pune veličine"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Promijeni alternativni tekst slike"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Alternativni tekst"

+ 26 - 0
packages/ckeditor5-image/lang/translations/hu.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Hungarian (https://www.transifex.com/ckeditor/teams/11143/hu/)\n"
+"Language: hu\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "képmodul"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Oldalsó kép"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Teljes méretű kép"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr ""
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr ""

+ 26 - 0
packages/ckeditor5-image/lang/translations/it.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Italian (https://www.transifex.com/ckeditor/teams/11143/it/)\n"
+"Language: it\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "Widget immagine"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Immagine laterale"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Immagine a dimensione intera"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr ""
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr ""

+ 26 - 0
packages/ckeditor5-image/lang/translations/ku.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Kurdish (https://www.transifex.com/ckeditor/teams/11143/ku/)\n"
+"Language: ku\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "وێدجیتی وێنە"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "لای وێنە"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "پڕ بەقەبارەی وێنە"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr ""
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr ""

+ 26 - 0
packages/ckeditor5-image/lang/translations/pt.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Portuguese (https://www.transifex.com/ckeditor/teams/11143/pt/)\n"
+"Language: pt\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "módulo de imagem"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Imagem lateral"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Imagem em tamanho completo"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr ""
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr ""

+ 26 - 0
packages/ckeditor5-image/lang/translations/uk.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Ukrainian (https://www.transifex.com/ckeditor/teams/11143/uk/)\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "Віджет зображення"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Бокове зображення"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Повний розмір зображення"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr ""
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr ""

+ 26 - 0
packages/ckeditor5-image/lang/translations/zh-cn.po

@@ -0,0 +1,26 @@
+# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language-Team: Chinese (China) (https://www.transifex.com/ckeditor/teams/11143/zh_CN/)\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "图像小部件"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr ""
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "全尺寸图像"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr ""
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr ""

+ 14 - 14
packages/ckeditor5-image/package.json

@@ -1,24 +1,24 @@
 {
   "name": "@ckeditor/ckeditor5-image",
-  "version": "0.3.0",
+  "version": "0.4.0",
   "description": "Image feature for CKEditor 5.",
   "keywords": [],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "*",
-    "@ckeditor/ckeditor5-engine": "*",
-    "@ckeditor/ckeditor5-ui": "*",
-    "@ckeditor/ckeditor5-utils": "*",
-    "@ckeditor/ckeditor5-theme-lark": "*"
+    "@ckeditor/ckeditor5-core": "^0.7.0",
+    "@ckeditor/ckeditor5-engine": "^0.8.0",
+    "@ckeditor/ckeditor5-ui": "^0.7.1",
+    "@ckeditor/ckeditor5-utils": "^0.8.0",
+    "@ckeditor/ckeditor5-theme-lark": "^0.6.1"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-clipboard": "*",
-    "@ckeditor/ckeditor5-dev-lint": "^2.0.0",
-    "@ckeditor/ckeditor5-editor-classic": "*",
-    "@ckeditor/ckeditor5-enter": "*",
-    "@ckeditor/ckeditor5-heading": "*",
-    "@ckeditor/ckeditor5-paragraph": "*",
-    "@ckeditor/ckeditor5-typing": "*",
-    "@ckeditor/ckeditor5-undo": "*",
+    "@ckeditor/ckeditor5-clipboard": "^0.4.1",
+    "@ckeditor/ckeditor5-dev-lint": "^2.0.2",
+    "@ckeditor/ckeditor5-editor-classic": "^0.7.1",
+    "@ckeditor/ckeditor5-enter": "^0.8.0",
+    "@ckeditor/ckeditor5-heading": "^0.8.0",
+    "@ckeditor/ckeditor5-paragraph": "^0.6.1",
+    "@ckeditor/ckeditor5-typing": "^0.8.0",
+    "@ckeditor/ckeditor5-undo": "^0.7.1",
     "gulp": "^3.9.1",
     "guppy-pre-commit": "^0.4.0"
   },

+ 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 - 31
packages/ckeditor5-image/src/converters.js

@@ -4,11 +4,12 @@
  */
 
 /**
- * @module image/converters
+ * @module image/image/converters
  */
 
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import { isImageWidget } from './utils';
+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:
@@ -57,36 +58,15 @@ export function viewToModelImage() {
 			modelImage.setAttribute( 'alt', viewImg.getAttribute( 'alt' ) );
 		}
 
-		data.output = modelImage;
-	};
-}
-
-/**
- * Returns model to view selection converter. This converter is applied after default selection conversion is made.
- * It creates fake view selection when {@link module:engine/view/selection~Selection#getSelectedElement} returns instance
- * of image widget.
- *
- * @param {Function} t {@link module:utils/locale~Locale#t Locale#t function} used to translate default fake selection's label.
- * @returns {Function}
- */
-export function modelToViewSelection( t ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewSelection = conversionApi.viewSelection;
-		const selectedElement = viewSelection.getSelectedElement();
-
-		if ( !selectedElement || !isImageWidget( selectedElement ) ) {
-			return;
-		}
+		// 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();
 
-		let fakeSelectionLabel = t( 'image widget' );
-		const imgElement = selectedElement.getChild( 0 );
-		const altText = imgElement.getAttribute( 'alt' );
-
-		if ( altText ) {
-			fakeSelectionLabel = `${ altText } ${ fakeSelectionLabel }`;
-		}
-
-		viewSelection.setFake( true, { label: fakeSelectionLabel } );
+		data.output = modelImage;
 	};
 }
 

+ 5 - 7
packages/ckeditor5-image/src/imageengine.js

@@ -4,13 +4,13 @@
  */
 
 /**
- * @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 { viewToModelImage, modelToViewSelection, createImageAttributeConverter } from './converters';
+import WidgetEngine from '../widget/widgetengine';
+import { viewToModelImage, createImageAttributeConverter } from './converters';
 import { toImageWidget } from './utils';
 import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
 import ViewEmptyElement from '@ckeditor/ckeditor5-engine/src/view/emptyelement';
@@ -39,6 +39,7 @@ export default class ImageEngine extends Plugin {
 		const schema = doc.schema;
 		const data = editor.data;
 		const editing = editor.editing;
+		const t = editor.t;
 
 		// Configure schema.
 		schema.registerItem( 'image' );
@@ -54,16 +55,13 @@ export default class ImageEngine extends Plugin {
 		// Build converter from model to view for editing pipeline.
 		buildModelConverter().for( editing.modelToView )
 			.fromElement( 'image' )
-			.toElement( () => toImageWidget( createImageViewElement() ) );
+			.toElement( () => toImageWidget( createImageViewElement(), t( 'image widget' ) ) );
 
 		createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'src' );
 		createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'alt' );
 
 		// Converter for figure element from view to model.
 		data.viewToModel.on( 'element:figure', viewToModelImage() );
-
-		// Creates fake selection label if selection is placed around image widget.
-		editing.modelToView.on( 'selection', modelToViewSelection( editor.t ), { priority: 'lowest' } );
 	}
 }
 

+ 17 - 13
packages/ckeditor5-image/src/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,30 +19,34 @@ 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'
+		};
+	}
 };
 
 /**
- * Image balloon panel class. It extends {module:ui/balloonpanel/balloonpanelview~BalloonPanelView} by adding helpers
+ * Image balloon panel class. It extends {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} by adding helpers
  * to use with image widgets. It sets proper positioning on `scroll` and `resize` events and hides the panel when
  * image is no longer selected or focus is lost.
  *
- * @extends module:ui/balloonpanel/balloonpanelview~BalloonPanelView
+ * @extends module:ui/panel/balloon/balloonpanelview~BalloonPanelView
  */
 export default class ImageBalloonPanelView extends BalloonPanelView {
 	/**

+ 13 - 5
packages/ckeditor5-image/src/utils.js

@@ -4,10 +4,10 @@
  */
 
 /**
- * @module image/utils
+ * @module image/image/utils
  */
 
-import { widgetize, isWidget } from './widget/utils';
+import { widgetize, isWidget } from '../widget/utils';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 
 const imageSymbol = Symbol( 'isImage' );
@@ -15,15 +15,23 @@ const imageSymbol = Symbol( 'isImage' );
 /**
  * Converts given {@link module:engine/view/element~Element} to image widget:
  * * adds {@link module:engine/view/element~Element#setCustomProperty custom property} allowing to recognize image widget element,
- * * calls {@link module:image/widget/utils~widgetize widgetize}.
+ * * calls {@link module:image/widget/utils~widgetize widgetize} function with proper element's label creator.
  *
  * @param {module:engine/view/element~Element} viewElement
+ * @param {String} label Element's label. It will be concatenated with image's `alt` attribute if one is present.
  * @returns {module:engine/view/element~Element}
  */
-export function toImageWidget( viewElement ) {
+export function toImageWidget( viewElement, label ) {
 	viewElement.setCustomProperty( imageSymbol, true );
 
-	return widgetize( viewElement );
+	return widgetize( viewElement, { label: labelCreator } );
+
+	function labelCreator() {
+		const imgElement = viewElement.getChild( 0 );
+		const altText = imgElement.getAttribute( 'alt' );
+
+		return altText ? `${ altText } ${ label }` : label;
+	}
 }
 
 /**

+ 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;
+}

+ 6 - 2
packages/ckeditor5-image/src/imagestyle/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';
 
 /**
@@ -40,6 +40,10 @@ export default class ImageStyle extends Plugin {
 		const defaultImageToolbarConfig = this.editor.config.get( 'image.defaultToolbar' );
 
 		if ( defaultImageToolbarConfig ) {
+			if ( defaultImageToolbarConfig.length ) {
+				defaultImageToolbarConfig.push( '|' );
+			}
+
 			styles.forEach( style => defaultImageToolbarConfig.push( style.name ) );
 		}
 	}

+ 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';

+ 27 - 23
packages/ckeditor5-image/src/imagealternatetext/imagealternatetext.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,16 @@ export default class ImageAlternateText extends Plugin {
 		const defaultImageToolbarConfig = this.editor.config.get( 'image.defaultToolbar' );
 
 		if ( defaultImageToolbarConfig ) {
-			defaultImageToolbarConfig.push( 'imageAlternateText' );
+			if ( defaultImageToolbarConfig.length ) {
+				defaultImageToolbarConfig.push( '|' );
+			}
+
+			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 +60,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 +98,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 +156,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

@@ -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

@@ -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;
 	}

+ 4 - 6
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 {
 	/**
@@ -69,9 +69,7 @@ export default class ImageToolbar extends Plugin {
 		promises.push( panel.content.add( toolbar ) );
 
 		// Add buttons to the toolbar.
-		for ( let name of toolbarConfig ) {
-			promises.push( toolbar.items.add( editor.ui.componentFactory.create( name ) ) );
-		}
+		promises.push( toolbar.fillFromConfig( toolbarConfig, editor.ui.componentFactory ) );
 
 		// Add balloon panel to editor's UI.
 		promises.push( editor.ui.view.body.add( panel ) );

+ 38 - 1
packages/ckeditor5-image/src/widget/utils.js

@@ -8,6 +8,7 @@
  */
 
 const widgetSymbol = Symbol( 'isWidget' );
+const labelSymbol = Symbol( 'label' );
 
 /**
  * CSS class added to each widget element.
@@ -41,17 +42,53 @@ export function isWidget( element ) {
  * * adds custom property allowing to recognize widget elements by using {@link ~isWidget}.
  *
  * @param {module:engine/view/element~Element} element
+ * @param {Object} [options]
+ * @param {String|Function} [options.label] Element's label provided to {@link ~setLabel} function. It can be passed as
+ * a plain string or a function returning a string.
  * @returns {module:engine/view/element~Element} Returns same element.
  */
-export function widgetize( element ) {
+export function widgetize( element, options ) {
+	options = options || {};
 	element.setAttribute( 'contenteditable', false );
 	element.getFillerOffset = getFillerOffset;
 	element.addClass( WIDGET_CLASS_NAME );
 	element.setCustomProperty( widgetSymbol, true );
 
+	if ( options.label ) {
+		setLabel( element, options.label );
+	}
+
 	return element;
 }
 
+/**
+ * Sets label for given element.
+ * It can be passed as a plain string or a function returning a string. Function will be called each time label is retrieved by
+ * {@link ~getLabel}.
+ *
+ * @param {module:engine/view/element~Element} element
+ * @param {String|Function} labelOrCreator
+ */
+export function setLabel( element, labelOrCreator ) {
+	element.setCustomProperty( labelSymbol, labelOrCreator );
+}
+
+/**
+ * Returns label for provided element.
+ *
+ * @param {module:engine/view/element~Element} element
+ * @return {String}
+ */
+export function getLabel( element ) {
+	const labelCreator = element.getCustomProperty( labelSymbol );
+
+	if ( !labelCreator ) {
+		return '';
+	}
+
+	return typeof labelCreator == 'function' ? labelCreator() : labelCreator;
+}
+
 // Default filler offset function applied to all widget elements.
 //
 // @returns {null}

+ 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;
+}

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

@@ -8,7 +8,7 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import { WIDGET_SELECTED_CLASS_NAME, isWidget } from './utils';
+import { WIDGET_SELECTED_CLASS_NAME, isWidget, getLabel } from './utils';
 
 /**
  * The widget engine plugin.
@@ -34,13 +34,15 @@ 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 ) ) {
 				return;
 			}
 
-			viewSelection.setFake( true );
+			viewSelection.setFake( true, { label: getLabel( selectedElement ) } );
 			selectedElement.addClass( WIDGET_SELECTED_CLASS_NAME );
 			previouslySelected = selectedElement;
 		}, { priority: 'low' } );

+ 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 - 54
packages/ckeditor5-image/tests/converters.js

@@ -3,14 +3,14 @@
  * For licensing, see LICENSE.md.
  */
 
-import { viewToModelImage, modelToViewSelection, 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;
@@ -32,7 +32,6 @@ describe( 'Image converters', () => {
 					.fromElement( 'image' )
 					.toElement( () => toImageWidget( createImageViewElement() ) );
 
-				editor.editing.modelToView.on( 'selection', modelToViewSelection( editor.t ), { priority: 'lowest' } );
 				buildModelConverter().for( editor.editing.modelToView )
 					.fromElement( 'image' )
 					.toElement( () => toImageWidget( createImageViewElement() ) );
@@ -52,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', () => {
@@ -72,54 +67,20 @@ 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( '' );
-		} );
-
-		function test( viewString, modelString ) {
-			const element = parseView( viewString );
-			const model = dispatcher.convert( element );
-
-			expect( stringifyModel( model ) ).to.equal( modelString );
-		}
-	} );
-
-	describe( 'modelToViewSelection', () => {
-		it( 'should convert selection over image to fake selection', () => {
-			setModelData( document, '[<image src=""></image>]' );
-
-			expect( viewDocument.selection.isFake ).to.be.true;
-			expect( viewDocument.selection.fakeSelectionLabel ).to.equal( 'image widget' );
-		} );
-
-		it( 'should convert fake selection label with alt', () => {
-			setModelData( document, '[<image src="" alt="foo bar"></image>]' );
-
-			expect( viewDocument.selection.isFake ).to.be.true;
-			expect( viewDocument.selection.fakeSelectionLabel ).to.equal( 'foo bar image widget' );
-		} );
-
-		it( 'should not convert fake selection if image is not selected', () => {
-			setModelData( document, '[]<image src="" alt="foo bar"></image>' );
-
-			expect( viewDocument.selection.isFake ).to.be.false;
+			editor.setData( '<figure class="image"></figure>' );
+			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
 		} );
 	} );
 

+ 11 - 2
packages/ckeditor5-image/tests/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

@@ -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/panel/balloon/balloonpanelview';
-import ImageEngine from '../../src/imageengine';
+import ImageEngine from '../../../src/image/imageengine';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'ImageBalloonPanel', () => {

+ 22 - 5
packages/ckeditor5-image/tests/utils.js

@@ -5,21 +5,38 @@
 
 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 } from '../src/widget/utils';
+import { toImageWidget, isImageWidget, isImage } from '../../src/image/utils';
+import { isWidget, getLabel } from '../../src/widget/utils';
 
 describe( 'image widget utils', () => {
-	let element;
+	let element, image;
 
 	beforeEach( () => {
-		element = new ViewElement( 'div' );
-		toImageWidget( element );
+		image = new ViewElement( 'img' );
+		element = new ViewElement( 'figure', null, image );
+		toImageWidget( element, 'image widget' );
 	} );
 
 	describe( 'toImageWidget()', () => {
 		it( 'should be widgetized', () => {
 			expect( isWidget( element ) ).to.be.true;
 		} );
+
+		it( 'should set element\'s label', () => {
+			expect( getLabel( element ) ).to.equal( 'image widget' );
+		} );
+
+		it( 'should set element\'s label combined with alt attribute', () => {
+			image.setAttribute( 'alt', 'foo bar baz' );
+			expect( getLabel( element ) ).to.equal( 'foo bar baz image widget' );
+		} );
+
+		it( 'provided label creator should always return same label', () => {
+			image.setAttribute( 'alt', 'foo bar baz' );
+
+			expect( getLabel( element ) ).to.equal( 'foo bar baz image widget' );
+			expect( getLabel( element ) ).to.equal( 'foo bar baz image widget' );
+		} );
 	} );
 
 	describe( 'isImageWidget()', () => {

+ 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;
+		} );
+	} );
+} );

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

@@ -3,10 +3,11 @@
  * For licensing, see LICENSE.md.
  */
 
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 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';
 
@@ -86,6 +87,28 @@ describe( 'ImageStyle', () => {
 			} );
 	} );
 
+	it( 'should add separator before the button if toolbar is present and already has items', () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		const FooPlugin = class extends Plugin {
+			init() {
+				this.editor.ui.componentFactory.add( 'foo', () => new ButtonView() );
+
+				this.editor.config.get( 'image.defaultToolbar' ).push( 'foo' );
+			}
+		};
+
+		return ClassicTestEditor.create( editorElement, {
+			plugins: [ FooPlugin, ImageStyle, ImageToolbar ]
+		} )
+		.then( newEditor => {
+			expect( newEditor.config.get( 'image.defaultToolbar' ) ).to.eql( [ 'foo', '|', 'imageStyleFull', 'imageStyleSide' ] );
+
+			newEditor.destroy();
+		} );
+	} );
+
 	it( 'should not add buttons to image toolbar if configuration is present', () => {
 		const editorElement = global.document.createElement( 'div' );
 		global.document.body.appendChild( editorElement );

+ 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';

+ 47 - 24
packages/ckeditor5-image/tests/imagealternatetext/imagealternatetext.js

@@ -3,11 +3,12 @@
  * For licensing, see LICENSE.md.
  */
 
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 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 +16,7 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 /* global Event */
 
-describe( 'ImageAlternateText', () => {
+describe( 'ImageTextAlternative', () => {
 	let editor, plugin, command, balloonPanel, form;
 
 	beforeEach( () => {
@@ -23,13 +24,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 +41,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 +102,32 @@ 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();
+			} );
+		} );
+
+		it( 'should add separator before the button if toolbar is present and already has items', () => {
+			const editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
+
+			const FooPlugin = class extends Plugin {
+				init() {
+					this.editor.ui.componentFactory.add( 'foo', () => new ButtonView() );
+
+					this.editor.config.get( 'image.defaultToolbar' ).push( 'foo' );
+				}
+			};
+
+			return ClassicTestEditor.create( editorElement, {
+				plugins: [ FooPlugin, ImageTextAlternative, ImageToolbar ]
+			} )
+			.then( newEditor => {
+				expect( newEditor.config.get( 'image.defaultToolbar' ) ).to.eql( [ 'foo', '|', 'imageTextAlternative' ] );
 
 				newEditor.destroy();
 			} );
@@ -117,7 +140,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 +155,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 +220,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 +244,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 +253,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

@@ -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

@@ -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

@@ -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.
-

File diff suppressed because it is too large
+ 11 - 0
packages/ckeditor5-image/tests/manual/caption.html


+ 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.

+ 3 - 3
packages/ckeditor5-image/tests/manual/image.html

@@ -1,14 +1,14 @@
 <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 image-style-side">
-		<img src="logo.png" alt="" />
+		<img src="sample.jpg" alt="" />
 	</figure>
 	<figure class="image">
-		<img src="logo.png" alt="" />
+		<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>
 	<figure class="image">
-		<img src="logo.png" alt="CKEditor logo" />
+		<img src="sample.jpg" alt="CKEditor logo" />
 	</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>

File diff suppressed because it is too large
+ 1 - 1
packages/ckeditor5-image/tests/manual/imagestyle.html


+ 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 );
+} );

BIN
packages/ckeditor5-image/tests/manual/logo.png


BIN
packages/ckeditor5-image/tests/manual/sample.jpg


File diff suppressed because it is too large
+ 1 - 1
packages/ckeditor5-image/tests/manual/alternatetext.html


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.
+

+ 40 - 1
packages/ckeditor5-image/tests/widget/utils.js

@@ -4,7 +4,7 @@
  */
 
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
-import { widgetize, isWidget, WIDGET_CLASS_NAME } from '../../src/widget/utils';
+import { widgetize, isWidget, WIDGET_CLASS_NAME, setLabel, getLabel } from '../../src/widget/utils';
 
 describe( 'widget utils', () => {
 	let element;
@@ -27,6 +27,20 @@ describe( 'widget utils', () => {
 		it( 'should add proper CSS class', () => {
 			expect( element.hasClass( WIDGET_CLASS_NAME ) ).to.be.true;
 		} );
+
+		it( 'should add element\'s label if one is provided', () => {
+			element = new ViewElement( 'div' );
+			widgetize( element, { label: 'foo bar baz label' } );
+
+			expect( getLabel( element ) ).to.equal( 'foo bar baz label' );
+		} );
+
+		it( 'should add element\'s label if one is provided as function', () => {
+			element = new ViewElement( 'div' );
+			widgetize( element, { label: () => 'foo bar baz label' } );
+
+			expect( getLabel( element ) ).to.equal( 'foo bar baz label' );
+		} );
 	} );
 
 	describe( 'isWidget()', () => {
@@ -38,4 +52,29 @@ describe( 'widget utils', () => {
 			expect( isWidget( new ViewElement( 'p' ) ) ).to.be.false;
 		} );
 	} );
+
+	describe( 'label utils', () => {
+		it( 'should allow to set label for element', () => {
+			const element = new ViewElement( 'p' );
+			setLabel( element, 'foo bar baz' );
+
+			expect( getLabel( element ) ).to.equal( 'foo bar baz' );
+		} );
+
+		it( 'should return empty string for elements without label', () => {
+			const element = new ViewElement( 'div' );
+
+			expect( getLabel( element ) ).to.equal( '' );
+		} );
+
+		it( 'should allow to use a function as label creator', () => {
+			const element = new ViewElement( 'p' );
+			let caption = 'foo';
+			setLabel( element, () => caption );
+
+			expect( getLabel( element ) ).to.equal( 'foo' );
+			caption = 'bar';
+			expect( getLabel( element ) ).to.equal( 'bar' );
+		} );
+	} );
 } );

+ 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 );

+ 37 - 1
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 } from '../../src/widget/utils';
 
 describe( 'WidgetEngine', () => {
@@ -23,10 +24,22 @@ 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' )
-					.toElement( () => widgetize( new ViewContainer( 'div' ) ) );
+					.toElement( () => {
+						const element = widgetize( new ViewContainer( 'div' ), { label: 'element label' } );
+
+						return element;
+					} );
+
+				buildModelConverter().for( editor.editing.modelToView )
+					.fromElement( 'editable' )
+					.toElement( () => new ViewEditable( 'figcaption', { contenteditable: true } ) );
 			} );
 	} );
 
@@ -43,6 +56,18 @@ describe( 'WidgetEngine', () => {
 		expect( viewDocument.selection.isFake ).to.be.true;
 	} );
 
+	it( 'should use element\'s label to set fake selection if one is provided', () => {
+		setModelData( document, '[<widget>foo bar</widget>]' );
+
+		expect( viewDocument.selection.fakeSelectionLabel ).to.equal( 'element label' );
+	} );
+
+	it( 'fake selection should be empty if widget is not selected', () => {
+		setModelData( document, '<widget>foo bar</widget>' );
+
+		expect( viewDocument.selection.fakeSelectionLabel ).to.equal( '' );
+	} );
+
 	it( 'should toggle selected class', () => {
 		setModelData( document, '[<widget>foo</widget>]' );
 
@@ -58,4 +83,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

@@ -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;