Przeglądaj źródła

Merge tag 'v1.0.0-beta.1' into stable

Release: v1.0.0-beta.1.
Piotrek Koszuliński 7 lat temu
rodzic
commit
46d0f51119
100 zmienionych plików z 4110 dodań i 2947 usunięć
  1. 1 1
      packages/ckeditor5-image/.eslintrc.js
  2. 15 11
      packages/ckeditor5-image/.travis.yml
  3. 32 0
      packages/ckeditor5-image/CHANGELOG.md
  4. 1 1
      packages/ckeditor5-image/LICENSE.md
  5. 3 1
      packages/ckeditor5-image/README.md
  6. 3 5
      packages/ckeditor5-image/docs/_snippets/features/image-caption.js
  7. 7 9
      packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js
  8. 3 5
      packages/ckeditor5-image/docs/_snippets/features/image-style.js
  9. 3 5
      packages/ckeditor5-image/docs/_snippets/features/image-toolbar.js
  10. 3 5
      packages/ckeditor5-image/docs/_snippets/features/image.js
  11. 30 18
      packages/ckeditor5-image/docs/features/image.md
  12. 3 1
      packages/ckeditor5-image/lang/contexts.json
  13. 9 1
      packages/ckeditor5-image/lang/translations/ast.po
  14. 12 4
      packages/ckeditor5-image/lang/translations/da.po
  15. 12 4
      packages/ckeditor5-image/lang/translations/de.po
  16. 9 1
      packages/ckeditor5-image/lang/translations/el.po
  17. 12 4
      packages/ckeditor5-image/lang/translations/en-au.po
  18. 9 1
      packages/ckeditor5-image/lang/translations/en.po
  19. 9 1
      packages/ckeditor5-image/lang/translations/eo.po
  20. 16 8
      packages/ckeditor5-image/lang/translations/es.po
  21. 50 0
      packages/ckeditor5-image/lang/translations/et.po
  22. 50 0
      packages/ckeditor5-image/lang/translations/eu.po
  23. 9 1
      packages/ckeditor5-image/lang/translations/fi.po
  24. 9 1
      packages/ckeditor5-image/lang/translations/fr.po
  25. 12 4
      packages/ckeditor5-image/lang/translations/gl.po
  26. 12 4
      packages/ckeditor5-image/lang/translations/hr.po
  27. 9 1
      packages/ckeditor5-image/lang/translations/hu.po
  28. 9 1
      packages/ckeditor5-image/lang/translations/it.po
  29. 9 1
      packages/ckeditor5-image/lang/translations/ja.po
  30. 9 1
      packages/ckeditor5-image/lang/translations/km.po
  31. 9 1
      packages/ckeditor5-image/lang/translations/kn.po
  32. 9 1
      packages/ckeditor5-image/lang/translations/ko.po
  33. 9 1
      packages/ckeditor5-image/lang/translations/ku.po
  34. 12 4
      packages/ckeditor5-image/lang/translations/nb.po
  35. 50 0
      packages/ckeditor5-image/lang/translations/ne.po
  36. 50 0
      packages/ckeditor5-image/lang/translations/nl.po
  37. 50 0
      packages/ckeditor5-image/lang/translations/pl.po
  38. 9 1
      packages/ckeditor5-image/lang/translations/pt-br.po
  39. 9 1
      packages/ckeditor5-image/lang/translations/pt.po
  40. 9 1
      packages/ckeditor5-image/lang/translations/ro.po
  41. 50 0
      packages/ckeditor5-image/lang/translations/ru.po
  42. 50 0
      packages/ckeditor5-image/lang/translations/si.po
  43. 9 1
      packages/ckeditor5-image/lang/translations/sk.po
  44. 50 0
      packages/ckeditor5-image/lang/translations/sq.po
  45. 50 0
      packages/ckeditor5-image/lang/translations/sv.po
  46. 12 4
      packages/ckeditor5-image/lang/translations/tr.po
  47. 50 0
      packages/ckeditor5-image/lang/translations/ug.po
  48. 9 1
      packages/ckeditor5-image/lang/translations/uk.po
  49. 9 1
      packages/ckeditor5-image/lang/translations/zh-cn.po
  50. 9 1
      packages/ckeditor5-image/lang/translations/zh.po
  51. 22 21
      packages/ckeditor5-image/package.json
  52. 5 25
      packages/ckeditor5-image/src/image.js
  53. 49 237
      packages/ckeditor5-image/src/image/converters.js
  54. 122 0
      packages/ckeditor5-image/src/image/imageediting.js
  55. 0 116
      packages/ckeditor5-image/src/image/imageengine.js
  56. 8 4
      packages/ckeditor5-image/src/image/ui/utils.js
  57. 6 5
      packages/ckeditor5-image/src/image/utils.js
  58. 5 4
      packages/ckeditor5-image/src/imagecaption.js
  59. 293 0
      packages/ckeditor5-image/src/imagecaption/imagecaptionediting.js
  60. 0 249
      packages/ckeditor5-image/src/imagecaption/imagecaptionengine.js
  61. 8 10
      packages/ckeditor5-image/src/imagecaption/utils.js
  62. 21 61
      packages/ckeditor5-image/src/imagestyle.js
  63. 34 86
      packages/ckeditor5-image/src/imagestyle/converters.js
  64. 36 26
      packages/ckeditor5-image/src/imagestyle/imagestylecommand.js
  65. 102 0
      packages/ckeditor5-image/src/imagestyle/imagestyleediting.js
  66. 0 298
      packages/ckeditor5-image/src/imagestyle/imagestyleengine.js
  67. 111 0
      packages/ckeditor5-image/src/imagestyle/imagestyleui.js
  68. 154 0
      packages/ckeditor5-image/src/imagestyle/utils.js
  69. 6 173
      packages/ckeditor5-image/src/imagetextalternative.js
  70. 6 10
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativecommand.js
  71. 3 3
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeediting.js
  72. 194 0
      packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js
  73. 19 21
      packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js
  74. 25 13
      packages/ckeditor5-image/src/imagetoolbar.js
  75. 39 0
      packages/ckeditor5-image/src/imageupload.js
  76. 67 0
      packages/ckeditor5-image/src/imageupload/imageuploadcommand.js
  77. 215 0
      packages/ckeditor5-image/src/imageupload/imageuploadediting.js
  78. 143 0
      packages/ckeditor5-image/src/imageupload/imageuploadprogress.js
  79. 60 0
      packages/ckeditor5-image/src/imageupload/imageuploadui.js
  80. 67 0
      packages/ckeditor5-image/src/imageupload/utils.js
  81. 24 22
      packages/ckeditor5-image/tests/image.js
  82. 145 217
      packages/ckeditor5-image/tests/image/converters.js
  83. 131 164
      packages/ckeditor5-image/tests/image/imageediting.js
  84. 17 10
      packages/ckeditor5-image/tests/image/ui/utils.js
  85. 11 8
      packages/ckeditor5-image/tests/image/utils.js
  86. 4 4
      packages/ckeditor5-image/tests/imagecaption.js
  87. 142 161
      packages/ckeditor5-image/tests/imagecaption/imagecaptionediting.js
  88. 11 7
      packages/ckeditor5-image/tests/imagecaption/utils.js
  89. 8 53
      packages/ckeditor5-image/tests/imagestyle.js
  90. 61 53
      packages/ckeditor5-image/tests/imagestyle/imagestylecommand.js
  91. 305 0
      packages/ckeditor5-image/tests/imagestyle/imagestyleediting.js
  92. 0 515
      packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js
  93. 135 0
      packages/ckeditor5-image/tests/imagestyle/imagestyleui.js
  94. 117 0
      packages/ckeditor5-image/tests/imagestyle/utils.js
  95. 8 196
      packages/ckeditor5-image/tests/imagetextalternative.js
  96. 26 24
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativecommand.js
  97. 6 5
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeediting.js
  98. 228 0
      packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js
  99. 2 2
      packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js
  100. 25 26
      packages/ckeditor5-image/tests/imagetoolbar.js

+ 1 - 1
packages/ckeditor5-image/.eslintrc.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 15 - 11
packages/ckeditor5-image/.travis.yml

@@ -1,24 +1,28 @@
 sudo: required
 dist: trusty
 addons:
+  firefox: "latest"
   apt:
     sources:
-      - google-chrome
+    - google-chrome
     packages:
-      - google-chrome-stable
+    - google-chrome-stable
 language: node_js
 node_js:
-  - "6"
+- '6'
 cache:
-  - node_modules
+- node_modules
 before_install:
-  - export DISPLAY=:99.0
-  - sh -e /etc/init.d/xvfb start
+- export DISPLAY=:99.0
+- sh -e /etc/init.d/xvfb start
 install:
-  - npm install @ckeditor/ckeditor5-dev-tests
-  - ckeditor5-dev-tests-install-dependencies
+- npm install @ckeditor/ckeditor5-dev-tests
+- ckeditor5-dev-tests-install-dependencies
 script:
-  - ckeditor5-dev-tests-travis
+- ckeditor5-dev-tests-travis
 after_success:
-  - codeclimate-test-reporter < coverage/lcov.info
-  - ckeditor5-dev-tests-save-revision
+- ckeditor5-dev-tests-save-revision
+env:
+  global:
+  - secure: PL9VtmtQsV0kOWE61Zl7O7elrcf1Rrvn0C5aIkeLAnVH94rZPKVXhz/KnScnqjjt+7yGsHdYL+0Lic6WgFei+Q19DKBpXsJ1Rkx7O98wxZxeGIZrP3tknnRHxcj6AJATst9h8lphw8J9aVwMSTCEDdCNbx4YjSfRWAflo3mT4De0VBrZrHz7vZ83XbOIsXtJMLo6UelEe+z8xhQy+qS5nrypBGEY1/Z8iAZR+kDaF1hp3tuzVUPRt87kVEo03RlyxEMNe7c9Q6Sodr0IJOLHl/z3Td7nK+g31xARUl7BNRHw5lKjNU6yEb3R4DEn83X0ljr7+4F9TbGTv0au9taotsnrImv/ciUpyBnPwD7eo2iU+7eQuEq4R9wfNtGUD/5USm0ElYeaV0UVGfEpIIb64o41xvlcZYN/8ZONOMwUXmStssfjbN1bc2yvEEQrMmgSA+ba44fpZB3gJXmoSxiG9xgudLFiXCmi0Q6lMHR53O8Zm+Gi0YgFeyvBsk5PiMQL78Tsnrf+mj7V8UUY8Xmz+hNhLC//HbmDF9F+X9cAhrlOVwRz75/0Nc3sc/YXIIU7PCkvQXewu+hjGkxlWlpjMkYOTXV07IHMZf4fW625Y+yJcML3z5s07/rmVJfHF83fZ5U620X9pOZSemm/puw4mb286HmPDLhIqrEtYM2FzuY=
+  - secure: YQc+VJSXN2FkDVuTb3gEDdBUf18zbOT9mA8B3IY+VOy3t33T1Fq4id92qPgOgcBriyjhzcJcwNQ1GSsmjofVci0h9cx0ltgnjU/t7X7AAAI95OZO2brawuH7w8ghdypuUM7A6iyPI5oy4JRwmRBfuKxiz+Tk8OOGBVkp3e04ynLOQZzxYVife8BOEpMx0eNUA/8VYPxOnspnqU+lKayrwaZU+nOG7uHz1dBoyom+wlWrh+78GiD4bYdzo/ftSAWvzYraeLbtmggjRAYKGMEBfIPG3NabX3DgvqB3xKujjHEhAG/MFfu8fTs4Z/jfrDlzDcoZ62L0vQ80fuEznoJciH4J53QeTooVdinp7gF43AaCyOpN9jJcdBCdoQN6HK2MVBuL3NlBX7ed5MFSbekr9/AF5ffYuIyrB5gEJKaIBiFIawgnpad7nJBzQT+21+p+t05cJXPqmuXTm+3LbKDBMhKXOXygcIBPVPcGCkGmS03Q2psNMOuXk8OMOXjW0BAYyW0kEWMs4/P+Gjy7mKHEXeyGaKVP1pWkrZsBragrnzzTpRcJNQyovUIAgMbScB7K1KtDpntfc0kzu+O46CfspGIdwOlfhzn5kHfzqJDldMe7PjtIOIev1HgYwL7DbVNxU0iFXEoDgJZGeiRYqe32ad7Z/RF9pj5qvKqq7pAJspM=

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

@@ -1,6 +1,38 @@
 Changelog
 =========
 
+## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-image/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
+
+### Features
+
+* Intorduced the `ImageUpload` feature. It was moved from the `@ckeditor/ckeditor5-upload` package. See [ckeditor/ckeditor5-upload#22](https://github.com/ckeditor/ckeditor5-upload/issues/22). ([b974bb0](https://github.com/ckeditor/ckeditor5-image/commit/b974bb0))
+* Simplified the text alternative UI, aligning the image package to the redesigned Lark theme (see [ckeditor/ckeditor5#645](https://github.com/ckeditor/ckeditor5/issues/645)). ([9a069b0](https://github.com/ckeditor/ckeditor5-image/commit/9a069b0))
+
+### Other changes
+
+* Aligned feature class naming to the new scheme. ([8690765](https://github.com/ckeditor/ckeditor5-image/commit/8690765))
+* Migrated package styles to PostCSS. Moved visual styles to `@ckeditor/ckeditor5-theme-lark` (see [ckeditor/ckeditor5-ui#144](https://github.com/ckeditor/ckeditor5-ui/issues/144)). ([ed6e1cf](https://github.com/ckeditor/ckeditor5-image/commit/ed6e1cf))
+* Removed the `.ck-editor-toolbar` and `.ck-editor-toolbar-container` classes from the UI (see [ckeditor/ckeditor5-theme-lark#135](https://github.com/ckeditor/ckeditor5-theme-lark/issues/135)). ([1c08fdd](https://github.com/ckeditor/ckeditor5-image/commit/1c08fdd))
+* Renamed `uploadImage` command and button to `imageUpload`. Closes [#184](https://github.com/ckeditor/ckeditor5-image/issues/184). ([6f891b8](https://github.com/ckeditor/ckeditor5-image/commit/6f891b8))
+* Updated naming of UI components & commands. ([2e7fbee](https://github.com/ckeditor/ckeditor5-image/commit/2e7fbee))
+* Updated translations. ([02f9cf5](https://github.com/ckeditor/ckeditor5-image/commit/02f9cf5))
+
+### BREAKING CHANGES
+
+* `uploadImage` command and button are now called `imageUpload`.
+* Renamed the `'imageUpload'` command to `'uploadImage'`.
+* The `'imageStyleFull'`, `'imageStyleSide'`, `'imageStyleAlignLeft'`, `'imageStyleAlignRight'` and `'imageStyleAlignCenter'` commands are no longer available. They were replaced by the `'imageStyle'` command that accepts name of an image style as a value.
+* The `'imageStyleFull'`, `'imageStyleSide'`, `'imageStyleAlignLeft'`, `'imageStyleAlignRight'` and `'imageStyleAlignCenter'` UI components are no longer available. Replaced by `'imageStyle:full'`, `'imageStyle:side'`, `'imageStyle:alignLeft'`, `'imageStyle:alignRight'` and `'imageStyle:alignCenter'`.
+* The `ImageStyleCommand#value` property is no longer a boolean only. Now it represents a name of an image style of the currently selected image element.
+* The `ImageStyleCommand` constructor's second parameter is now an array of supported image styles.
+* The DOM structure of the text alternative form has changed.
+
+
+## 0.0.1 (2017-11-06)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-image/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 
 ### Bug fixes

+ 1 - 1
packages/ckeditor5-image/LICENSE.md

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 
 **CKEditor 5 Image Feature** – https://github.com/ckeditor/ckeditor5-image <br>
-Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of any of the following licenses at your choice:
 

+ 3 - 1
packages/ckeditor5-image/README.md

@@ -4,7 +4,9 @@ CKEditor 5 image feature
 [![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-image.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-image)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-image.svg)](https://travis-ci.org/ckeditor/ckeditor5-image)
-[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-image/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-image/coverage)
+[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://www.browserstack.com/automate/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
+[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-image/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-image?branch=master)
+<br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-image/status.svg)](https://david-dm.org/ckeditor/ckeditor5-image)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-image/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-image?type=dev)
 

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

@@ -1,11 +1,11 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /* globals ClassicEditor, console, window, document */
 
-import { TOKEN_URL } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-caption' ), {
@@ -16,9 +16,7 @@ ClassicEditor
 		toolbar: {
 			viewportTopOffset: 60
 		},
-		cloudServices: {
-			tokenUrl: TOKEN_URL
-		}
+		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
 		window.editorCaption = editor;

+ 7 - 9
packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js

@@ -1,34 +1,32 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /* globals ClassicEditor, console, window, document */
 
-import { TOKEN_URL } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style-custom' ), {
 		image: {
 			styles: [
 				// This option is equal to a situation where no style is applied.
-				'imageStyleFull',
+				'full',
 
 				// This represents an image aligned to left.
-				'imageStyleAlignLeft',
+				'alignLeft',
 
 				// This represents an image aligned to right.
-				'imageStyleAlignRight'
+				'alignRight'
 			],
 
-			toolbar: [ 'imageTextAlternative', '|', 'imageStyleAlignLeft', 'imageStyleFull', 'imageStyleAlignRight' ]
+			toolbar: [ 'imageTextAlternative', '|', 'imageStyle:alignLeft', 'imageStyle:full', 'imageStyle:alignRight' ]
 		},
 		toolbar: {
 			viewportTopOffset: 60
 		},
-		cloudServices: {
-			tokenUrl: TOKEN_URL
-		}
+		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
 		window.editorStyleCustom = editor;

+ 3 - 5
packages/ckeditor5-image/docs/_snippets/features/image-style.js

@@ -1,20 +1,18 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /* globals ClassicEditor, console, window, document */
 
-import { TOKEN_URL } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-style' ), {
 		toolbar: {
 			viewportTopOffset: 60
 		},
-		cloudServices: {
-			tokenUrl: TOKEN_URL
-		}
+		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
 		window.editorStyle = editor;

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

@@ -1,11 +1,11 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /* globals ClassicEditor, console, window, document */
 
-import { TOKEN_URL } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image-toolbar' ), {
@@ -16,9 +16,7 @@ ClassicEditor
 		toolbar: {
 			viewportTopOffset: 60
 		},
-		cloudServices: {
-			tokenUrl: TOKEN_URL
-		}
+		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
 		window.editorToolbar = editor;

+ 3 - 5
packages/ckeditor5-image/docs/_snippets/features/image.js

@@ -1,11 +1,11 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /* globals ClassicEditor, console, window, document */
 
-import { TOKEN_URL } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloudservices/tests/_utils/cloudservices-config.js';
 
 ClassicEditor
 	.create( document.querySelector( '#snippet-image' ), {
@@ -13,9 +13,7 @@ ClassicEditor
 		toolbar: {
 			viewportTopOffset: 60
 		},
-		cloudServices: {
-			tokenUrl: TOKEN_URL
-		}
+		cloudServices: CS_CONFIG
 	} )
 	.then( editor => {
 		window.editorBasic = editor;

+ 30 - 18
packages/ckeditor5-image/docs/features/image.md

@@ -11,10 +11,10 @@ The [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckedit
 * {@link module:image/imagetoolbar~ImageToolbar} adds the image feature's contextual toolbar,
 * {@link module:image/imagecaption~ImageCaption} adds support for captions,
 * {@link module:image/imagestyle~ImageStyle} adds support for image styles,
-* {@link module:upload/imageupload~ImageUpload} adds support for uploading dropped or pasted images (note: it is currently located in the [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package but will be moved to the `@ckeditor/ckeditor5-image` package).
+* {@link module:image/imageupload~ImageUpload} adds support for uploading dropped or pasted images (see: {@link features/image-upload Image upload}).
 
 <info-box info>
-	The first four features listed above (so all except the upload support) are enabled by default in all builds.
+	All features listed above are enabled by default in all builds.
 </info-box>
 
 ## Base image support
@@ -100,7 +100,7 @@ A side image:
 <info-box>
 	The actual styling of the images is the developer's job. The editor comes with some default styles, but they will only be applied to images inside the editor. The developer needs to style them on the target pages.
 
-	Here you can find the source of the default styles applied by the editor: [`ckeditor5-image/theme/theme.scss`](https://github.com/ckeditor/ckeditor5-image/blob/master/theme/theme.scss).
+	Here you can find the source of the default styles applied by the editor: [`ckeditor5-image/theme/imagestyle.css`](https://github.com/ckeditor/ckeditor5-image/blob/master/theme/imagestyle.css).
 </info-box>
 
 Below you can see a demo of the editor with the image styles feature enabled. The default configuration is used. You can change the styles of images through the image's contextual toolbar.
@@ -118,17 +118,17 @@ ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		image: {
 			// You need to configure the image toolbar too, so it uses the new style buttons.
-			toolbar: [ 'imageTextAlternative', '|', 'imageStyleAlignLeft', 'imageStyleFull', 'imageStyleAlignRight' ],
+			toolbar: [ 'imageTextAlternative', '|', 'imageStyle:alignLeft', 'imageStyle:full', 'imageStyle:alignRight' ],
 
 			styles: [
 				// This option is equal to a situation where no style is applied.
-				'imageStyleFull',
+				'full',
 
 				// This represents an image aligned to left.
-				'imageStyleAlignLeft',
+				'alignLeft',
 
 				// This represents an image aligned to right.
-				'imageStyleAlignRight'
+				'alignRight'
 			]
 		}
 	} )
@@ -136,7 +136,7 @@ ClassicEditor
 	.catch( ... );
 ```
 
-In the code sample above we used predefined image styles – `'imageStyleFull'`, `'imageStyleAlignLeft'` and `'imageStyleAlignRight'`. The latter two apply, respectively, `.image-style-align-left` and  `.image-style-align-right` classes to the `<figure>` element.
+In the code sample above we used predefined image styles – `'full'`, `'alignLeft'` and `'alignRight'`. The latter two apply, respectively, `.image-style-align-left` and  `.image-style-align-right` classes to the `<figure>` element.
 
 See the result below:
 
@@ -148,13 +148,13 @@ See the result below:
 
 ### Defining custom styles
 
-Besides using the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles 5 predefined styles}:
+Besides using the {@link module:image/imagestyle/utils~defaultStyles 5 predefined styles}:
 
-* `'imageStyleFull'`,
-* `'imageStyleSide'`,
-* `'imageStyleAlignLeft'`,
-* `'imageStyleAlignCenter'`,
-* `'imageStyleAlignRight'`
+* `'full'`,
+* `'side'`,
+* `'alignLeft'`,
+* `'alignCenter'`,
+* `'alignRight'`
 
 you can also define your own styles or modify the existing ones.
 
@@ -162,7 +162,9 @@ you can also define your own styles or modify the existing ones.
 	Reusing (or modifying) predefined styles has this advantage that CKEditor 5 will use its official translations for the defined button titles.
 </info-box>
 
-TODO (example)...
+You can find advanced examples in the {@link module:image/image~ImageConfig#styles `image.styles`} configuration option's documentation.
+
+TODO (live example)...
 
 ## Image upload
 
@@ -194,7 +196,7 @@ ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Image, ImageToolbar, ImageCaption, ImageStyle ],
 		image: {
-			toolbar: [ 'imageTextAlternative', '|', 'imageStyleFull', 'imageStyleSide' ]
+			toolbar: [ 'imageTextAlternative', '|', 'imageStyle:full', 'imageStyle:side' ]
 		}
 	} )
 	.then( ... )
@@ -208,11 +210,21 @@ If you are using an editor build, see how to {@link builds/guides/development/cu
 The {@link module:image/image~Image} plugin registers:
 
 * The `'imageTextAlternative'` button.
+* * The {@link module:image/imagetextalternative/imagetextalternativecommand~ImageTextAlternativeCommand `'imageTextAlternative'` command}
 
 The {@link module:image/imagestyle~ImageStyle} plugin registers:
 
-* A command for each defined style (based on the {@link module:image/image~ImageConfig#styles `image.styles`} configuration option) &mdash; e.g. `'imageStyleFull'` and `'imageStyleSide'`,
-* A button for each defined style &mdash; e.g. `'imageStyleFull'` and `'imageStyleSide'`.
+* A button for each defined style &mdash; e.g. `'imageStyle:full'` and `'imageStyle:side'`.
+* The {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand `'imageStyle'` command} which accepts value based on the {@link module:image/image~ImageConfig#styles `image.styles`} configuration option (e.g. `'full'` and `'side'`):
+
+	```js
+	editor.execute( 'imageStyle', { value: 'side' } );
+	```
+
+The {@link module:image/imageupload~ImageUpload} plugin registers:
+
+* The `'imageUpload'` button which opens the native file browser to ley you upload a file directly from your disk.
+* The {@link module:image/imageupload/imageuploadcommand~ImageUploadCommand `'imageUpload'` command} which accepts the file to upload.
 
 ## Contribute
 

+ 3 - 1
packages/ckeditor5-image/lang/contexts.json

@@ -7,5 +7,7 @@
 	"Right aligned image": "Label for the Right aligned image option",
 	"Change image text alternative": "Label for the Change image text alternative button.",
 	"Text alternative": "Label for the image text alternative input.",
-	"Enter image caption": "Placeholder text for image caption displayed when caption is empty."
+	"Enter image caption": "Placeholder text for image caption displayed when caption is empty.",
+	"Insert image": "Label for the insert image toolbar button.",
+	"Upload failed": "Title of the notification displayed when upload fails."
 }

+ 9 - 1
packages/ckeditor5-image/lang/translations/ast.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ast\n"
@@ -40,3 +40,11 @@ msgstr ""
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr ""
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr ""
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 12 - 4
packages/ckeditor5-image/lang/translations/da.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: da\n"
@@ -19,15 +19,15 @@ msgstr "Fuld billedstørrelse"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "Venstrestillet billede"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "Centreret billede"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "Højrestillet billede"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -40,3 +40,11 @@ msgstr "Alternativ tekst"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Indtast billedoverskrift"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Indsæt billede"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Upload fejlede"

+ 12 - 4
packages/ckeditor5-image/lang/translations/de.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: de\n"
@@ -19,15 +19,15 @@ msgstr "Bild in voller Größe"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "linksbündiges Bild"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "zentriertes Bild"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "rechtsbündiges Bild"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -40,3 +40,11 @@ msgstr "Textalternative"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Bildunterschrift eingeben"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Bild einfügen"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 9 - 1
packages/ckeditor5-image/lang/translations/el.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: el\n"
@@ -40,3 +40,11 @@ msgstr "Εναλλακτικό κείμενο"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Λεζάντα"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Εισαγωγή εικόνας"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 12 - 4
packages/ckeditor5-image/lang/translations/en-au.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: en_AU\n"
@@ -19,15 +19,15 @@ msgstr "Full size image"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "Left aligned image"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "Centred image"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "Right aligned image"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -40,3 +40,11 @@ msgstr "Text alternative"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Enter image caption"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Insert image"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Upload failed"

+ 9 - 1
packages/ckeditor5-image/lang/translations/en.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: \n"
@@ -40,3 +40,11 @@ msgstr "Text alternative"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Enter image caption"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Insert image"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Upload failed"

+ 9 - 1
packages/ckeditor5-image/lang/translations/eo.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: eo\n"
@@ -40,3 +40,11 @@ msgstr "Alternativa teksto"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Skribu klarigon pri la bildo"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Enmetu bildon"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 16 - 8
packages/ckeditor5-image/lang/translations/es.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: es\n"
@@ -7,27 +7,27 @@ msgstr ""
 
 msgctxt "Label for the image widget."
 msgid "image widget"
-msgstr "Imagen de widget"
+msgstr "Widget de imagen"
 
 msgctxt "Label for the Side image option."
 msgid "Side image"
-msgstr ""
+msgstr "Imagen lateral"
 
 msgctxt "Label for the Full size image option."
 msgid "Full size image"
-msgstr "Imagen tamaño completo"
+msgstr "Imagen a tamaño completo"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "Imagen alineada a la izquierda"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "Imagen centrada"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "Imagen alineada a la derecha"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -39,4 +39,12 @@ msgstr "Texto alternativo"
 
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
-msgstr ""
+msgstr "Introducir título de la imagen"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Insertar imagen"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Fallo en la subida"

+ 50 - 0
packages/ckeditor5-image/lang/translations/et.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: et\n"
+"Language-Team: Estonian (https://www.transifex.com/ckeditor/teams/11143/et/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "pildi vidin"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Pilt küljel"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Täissuuruses pilt"
+
+msgctxt "Label for the Left aligned image option"
+msgid "Left aligned image"
+msgstr "Vasakule joondatud pilt"
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr "Keskele joondatud pilt"
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned image"
+msgstr "Paremale joondatud pilt"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Muuda pildi tekstilist alternatiivi"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Alternatiivne tekst"
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr "Sisesta pildi pealkiri"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Siseta pilt"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Üleslaadimine ebaõnnestus"

+ 50 - 0
packages/ckeditor5-image/lang/translations/eu.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: eu\n"
+"Language-Team: Basque (https://www.transifex.com/ckeditor/teams/11143/eu/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "irudi widgeta"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Alboko irudia"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Tamaina osoko irudia"
+
+msgctxt "Label for the Left aligned image option"
+msgid "Left aligned image"
+msgstr "Ezkerrean lerrokatutako irudia"
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr "Zentratutako irudia"
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned image"
+msgstr "Eskuinean lerrokatutako irudia"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Aldatu irudiaren ordezko testua"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Ordezko testua"
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr "Sartu irudiaren epigrafea"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Txertatu irudia"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Kargatzeak huts egin du"

+ 9 - 1
packages/ckeditor5-image/lang/translations/fi.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: fi\n"
@@ -40,3 +40,11 @@ msgstr "Vaihtoehtoinen teksti"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Syötä kuvateksti"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Lisää kuva"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Lataus epäonnistui"

+ 9 - 1
packages/ckeditor5-image/lang/translations/fr.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: fr\n"
@@ -40,3 +40,11 @@ msgstr "Texte alternatif"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Saisissez la légende de l’image"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Insérer une image"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Échec de l'envoi"

+ 12 - 4
packages/ckeditor5-image/lang/translations/gl.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: gl\n"
@@ -19,15 +19,15 @@ msgstr "Imaxe a tamaño completo"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "Imaxe aliñada á esquerda"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "Imaxe centrada horizontalmente"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "Imaxe aliñada á dereita"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -40,3 +40,11 @@ msgstr "Texto alternativo"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Introduza o título da imaxe"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Inserir imaxe"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Fallou o envío"

+ 12 - 4
packages/ckeditor5-image/lang/translations/hr.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: hr\n"
@@ -19,15 +19,15 @@ msgstr "Slika pune veličine"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "Lijevo poravnata slika"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "Centrirana slika"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "Slika poravnata desno"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -40,3 +40,11 @@ msgstr "Alternativni tekst"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Unesite naslov slike"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Umetni sliku"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Slanje nije uspjelo"

+ 9 - 1
packages/ckeditor5-image/lang/translations/hu.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: hu\n"
@@ -40,3 +40,11 @@ msgstr "Helyettesítő szöveg"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Képaláírás megadása"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Kép beszúrása"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "A feltöltés nem sikerült"

+ 9 - 1
packages/ckeditor5-image/lang/translations/it.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: it\n"
@@ -40,3 +40,11 @@ msgstr "Testo alternativo"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "inserire didascalia dell'immagine"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Inserisci immagine"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Caricamento fallito"

+ 9 - 1
packages/ckeditor5-image/lang/translations/ja.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ja\n"
@@ -40,3 +40,11 @@ msgstr "代替テキスト"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "画像の注釈を入力"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "画像挿入"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "アップロード失敗"

+ 9 - 1
packages/ckeditor5-image/lang/translations/km.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: km\n"
@@ -40,3 +40,11 @@ msgstr ""
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "បញ្ចូល​ពាក្យ​ពណ៌នា​រូបភាព"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "បញ្ចូល​រូបភាព"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "អាប់ឡូត​មិនបាន"

+ 9 - 1
packages/ckeditor5-image/lang/translations/kn.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: kn\n"
@@ -40,3 +40,11 @@ msgstr "‍ಪಠ್ಯದ ಬದಲಿ"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "‍ಚಿತ್ರದ ಶೀರ್ಷಿಕೆ ಸೇರಿಸು"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr ""
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 9 - 1
packages/ckeditor5-image/lang/translations/ko.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ko\n"
@@ -40,3 +40,11 @@ msgstr "대체 텍스트"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "이미지 설명을 입력하세요"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr ""
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 9 - 1
packages/ckeditor5-image/lang/translations/ku.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ku\n"
@@ -40,3 +40,11 @@ msgstr "جێگرەوەی تێکست"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "سەردێڕی وێنە دابنێ"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "وێنە دابنێ"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "بارکردنەکە سەرنەکەووت"

+ 12 - 4
packages/ckeditor5-image/lang/translations/nb.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: nb\n"
@@ -19,15 +19,15 @@ msgstr "Bilde i full størrelse"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "Venstrejustert bilde"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "Midtstilt bilde"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "Høyrejustert bilde"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -40,3 +40,11 @@ msgstr "Tekstalternativ for bilde"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Skriv inn bildetekst"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Sett inn bilde"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Opplasting feilet"

+ 50 - 0
packages/ckeditor5-image/lang/translations/ne.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: ne_NP\n"
+"Language-Team: Nepali (Nepal) (https://www.transifex.com/ckeditor/teams/11143/ne_NP/)\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 Left aligned image option"
+msgid "Left aligned image"
+msgstr ""
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr ""
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned 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 ""
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr ""
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "तस्वीर सम्मिलित गर्नुहोस्"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 50 - 0
packages/ckeditor5-image/lang/translations/nl.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: nl\n"
+"Language-Team: Dutch (https://www.transifex.com/ckeditor/teams/11143/nl/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "Afbeeldingswidget"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Afbeelding naast tekst"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Afbeelding op volledige grootte"
+
+msgctxt "Label for the Left aligned image option"
+msgid "Left aligned image"
+msgstr "Links uitgelijnde afbeelding"
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr "Gecentreerde afbeelding"
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned image"
+msgstr "Rechts uitgelijnde afbeelding"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Verander alt-tekst van de afbeelding"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Alt-tekst"
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr "Typ een afbeeldingsbijschrift"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Afbeelding toevoegen"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Opladen afbeelding mislukt"

+ 50 - 0
packages/ckeditor5-image/lang/translations/pl.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: pl\n"
+"Language-Team: Polish (https://www.transifex.com/ckeditor/teams/11143/pl/)\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\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 Left aligned image option"
+msgid "Left aligned image"
+msgstr ""
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr ""
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned 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 ""
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr ""
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Wstaw obraz"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 9 - 1
packages/ckeditor5-image/lang/translations/pt-br.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: pt_BR\n"
@@ -40,3 +40,11 @@ msgstr "Texto alternativo"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Inserir legenda da imagem"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Inserir imagem"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Falha ao subir arquivo"

+ 9 - 1
packages/ckeditor5-image/lang/translations/pt.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: pt\n"
@@ -40,3 +40,11 @@ msgstr "Texto alternativo"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Indicar legenda da imagem"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Inserir imagem"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Falha ao carregar"

+ 9 - 1
packages/ckeditor5-image/lang/translations/ro.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ro\n"
@@ -40,3 +40,11 @@ msgstr "Text alternativ"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Introdu titlul descriptiv al imaginii"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Inserează imagine"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Încărcare eșuată"

+ 50 - 0
packages/ckeditor5-image/lang/translations/ru.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: ru\n"
+"Language-Team: Russian (https://www.transifex.com/ckeditor/teams/11143/ru/)\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\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 Left aligned image option"
+msgid "Left aligned image"
+msgstr "Выравнивание по левому краю"
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr "Выравнивание по центру"
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned 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 "Альтернативный текст"
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr "Подпись к изображению"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Вставить изображение"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Загрузка не выполнена"

+ 50 - 0
packages/ckeditor5-image/lang/translations/si.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: si_LK\n"
+"Language-Team: Sinhala (Sri Lanka) (https://www.transifex.com/ckeditor/teams/11143/si_LK/)\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 Left aligned image option"
+msgid "Left aligned image"
+msgstr ""
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr ""
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned 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 ""
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr ""
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "පින්තූරය ඇතුල් කරන්න"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "උඩුගත කිරීම අසාර්ථක විය"

+ 9 - 1
packages/ckeditor5-image/lang/translations/sk.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: sk\n"
@@ -40,3 +40,11 @@ msgstr "Alternatívny text"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Vložte popis obrázka"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Vložiť obrázok"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Nahrávanie zlyhalo"

+ 50 - 0
packages/ckeditor5-image/lang/translations/sq.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: sq\n"
+"Language-Team: Albanian (https://www.transifex.com/ckeditor/teams/11143/sq/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Label for the image widget."
+msgid "image widget"
+msgstr "Vegla e fotos"
+
+msgctxt "Label for the Side image option."
+msgid "Side image"
+msgstr "Foto anësore"
+
+msgctxt "Label for the Full size image option."
+msgid "Full size image"
+msgstr "Foto me madhësi të plotë"
+
+msgctxt "Label for the Left aligned image option"
+msgid "Left aligned image"
+msgstr "Foto e vendosur majtas"
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr "Foto e vendosur në mes"
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned image"
+msgstr "Foto e vendosur djathtas"
+
+msgctxt "Label for the Change image text alternative button."
+msgid "Change image text alternative"
+msgstr "Ndrysho tekstin alternativ të fotos"
+
+msgctxt "Label for the image text alternative input."
+msgid "Text alternative"
+msgstr "Teksti alternativ"
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr "Shto përshkrimin e fotos"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Shto Foto"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Ngarkimi dishtoi"

+ 50 - 0
packages/ckeditor5-image/lang/translations/sv.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: sv\n"
+"Language-Team: Swedish (https://www.transifex.com/ckeditor/teams/11143/sv/)\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 Left aligned image option"
+msgid "Left aligned image"
+msgstr ""
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr ""
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned 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 ""
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr ""
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Infoga bild"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Uppladdning misslyckades"

+ 12 - 4
packages/ckeditor5-image/lang/translations/tr.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: tr\n"
@@ -19,15 +19,15 @@ msgstr "Tam Boyut Görsel"
 
 msgctxt "Label for the Left aligned image option"
 msgid "Left aligned image"
-msgstr ""
+msgstr "Sola hizalı görsel"
 
 msgctxt "Label for the Centered image option"
 msgid "Centered image"
-msgstr ""
+msgstr "Ortalı görsel"
 
 msgctxt "Label for the Right aligned image option"
 msgid "Right aligned image"
-msgstr ""
+msgstr "Sağa hizalı görsel"
 
 msgctxt "Label for the Change image text alternative button."
 msgid "Change image text alternative"
@@ -40,3 +40,11 @@ msgstr "Yazı alternatifi"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "Resim Açıklaması Gir"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "Görsel Ekle"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "Yükleme başarsız"

+ 50 - 0
packages/ckeditor5-image/lang/translations/ug.po

@@ -0,0 +1,50 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: ug\n"
+"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\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 Left aligned image option"
+msgid "Left aligned image"
+msgstr "سولغا توغۇرلانغان رەسىم"
+
+msgctxt "Label for the Centered image option"
+msgid "Centered image"
+msgstr "ئوتتۇردىكى رەسىم"
+
+msgctxt "Label for the Right aligned image option"
+msgid "Right aligned 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 "تېكىست ئاملاشتۇرۇش"
+
+msgctxt "Placeholder text for image caption displayed when caption is empty."
+msgid "Enter image caption"
+msgstr "رەسىمنىڭ تېمىسىنى كىرگۈزۈڭ"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "رەسىم قىستۇرۇش"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "چىقىرىش مەغلۇپ بولدى"

+ 9 - 1
packages/ckeditor5-image/lang/translations/uk.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: uk\n"
@@ -40,3 +40,11 @@ msgstr ""
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr ""
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr ""
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr ""

+ 9 - 1
packages/ckeditor5-image/lang/translations/zh-cn.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: zh_CN\n"
@@ -40,3 +40,11 @@ msgstr "替换文本"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "输入图片标题"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "插入图像"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "上传失败"

+ 9 - 1
packages/ckeditor5-image/lang/translations/zh.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: zh_TW\n"
@@ -40,3 +40,11 @@ msgstr "文字替代"
 msgctxt "Placeholder text for image caption displayed when caption is empty."
 msgid "Enter image caption"
 msgstr "輸入圖片說明"
+
+msgctxt "Label for the insert image toolbar button."
+msgid "Insert image"
+msgstr "插入圖片"
+
+msgctxt "Title of the notification displayed when upload fails."
+msgid "Upload failed"
+msgstr "上傳失敗"

+ 22 - 21
packages/ckeditor5-image/package.json

@@ -1,35 +1,36 @@
 {
   "name": "@ckeditor/ckeditor5-image",
-  "version": "1.0.0-alpha.2",
+  "version": "1.0.0-beta.1",
   "description": "Image feature for CKEditor 5.",
   "keywords": [
     "ckeditor5",
     "ckeditor5-feature"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-engine": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-ui": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-utils": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-theme-lark": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-widget": "^1.0.0-alpha.2"
+    "@ckeditor/ckeditor5-core": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-engine": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-ui": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-utils": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-theme-lark": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-upload": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-widget": "^1.0.0-beta.1"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-clipboard": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-enter": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-essentials": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-heading": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-link": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-list": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-undo": "^1.0.0-alpha.2",
-    "eslint": "^4.8.0",
-    "eslint-config-ckeditor5": "^1.0.6",
+    "@ckeditor/ckeditor5-basic-styles": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-clipboard": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-editor-classic": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-enter": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-essentials": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-heading": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-link": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-list": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-paragraph": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-typing": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-undo": "^1.0.0-beta.1",
+    "eslint": "^4.15.0",
+    "eslint-config-ckeditor5": "^1.0.7",
     "husky": "^0.14.3",
-    "lint-staged": "^4.2.3"
+    "lint-staged": "^6.0.0"
   },
   "engines": {
     "node": ">=6.0.0",

+ 5 - 25
packages/ckeditor5-image/src/image.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,17 +8,16 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageEngine from './image/imageengine';
+import ImageEditing from '../src/image/imageediting';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import ImageTextAlternative from './imagetextalternative';
-import { isImageWidgetSelected } from './image/utils';
 
-import '../theme/theme.scss';
+import '../theme/image.css';
 
 /**
  * The image plugin.
  *
- * Uses the {@link module:image/image/imageengine~ImageEngine}.
+ * Uses the {@link module:image/image/imageediting~ImageEditing}.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -27,7 +26,7 @@ export default class Image extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageEngine, Widget, ImageTextAlternative ];
+		return [ ImageEditing, Widget, ImageTextAlternative ];
 	}
 
 	/**
@@ -36,25 +35,6 @@ export default class Image extends Plugin {
 	static get pluginName() {
 		return 'Image';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const contextualToolbar = editor.plugins.get( 'ContextualToolbar' );
-
-		// If `ContextualToolbar` plugin is loaded, it should be disabled for images
-		// which have their own toolbar to avoid duplication.
-		// https://github.com/ckeditor/ckeditor5-image/issues/110
-		if ( contextualToolbar ) {
-			this.listenTo( contextualToolbar, 'show', evt => {
-				if ( isImageWidgetSelected( editor.editing.view.selection ) ) {
-					evt.stop();
-				}
-			}, { priority: 'high' } );
-		}
-	}
 }
 
 /**

+ 49 - 237
packages/ckeditor5-image/src/image/converters.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,8 +8,7 @@
  */
 
 import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
-import ModelDocumentFragment from '@ckeditor/ckeditor5-engine/src/model/documentfragment';
-import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
+import first from '@ckeditor/ckeditor5-utils/src/first';
 
 /**
  * Returns a function that converts the image view representation:
@@ -26,57 +25,43 @@ import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer';
  * @returns {Function}
  */
 export function viewFigureToModel() {
-	return ( evt, data, consumable, conversionApi ) => {
-		// Do not convert if this is not an "image figure".
-		if ( !consumable.test( data.input, { name: true, class: 'image' } ) ) {
-			return;
-		}
+	return dispatcher => {
+		dispatcher.on( 'element:figure', converter );
+	};
 
-		// Do not convert if image cannot be placed in model at this context.
-		if ( !conversionApi.schema.check( { name: 'image', inside: data.context, attributes: 'src' } ) ) {
+	function converter( evt, data, conversionApi ) {
+		// Do not convert if this is not an "image figure".
+		if ( !conversionApi.consumable.test( data.viewItem, { name: true, class: 'image' } ) ) {
 			return;
 		}
 
 		// Find an image element inside the figure element.
-		const viewImage = Array.from( data.input.getChildren() ).find( viewChild => viewChild.is( 'img' ) );
+		const viewImage = Array.from( data.viewItem.getChildren() ).find( viewChild => viewChild.is( 'img' ) );
 
 		// Do not convert if image element is absent, is missing src attribute or was already converted.
-		if ( !viewImage || !viewImage.hasAttribute( 'src' ) || !consumable.test( viewImage, { name: true } ) ) {
+		if ( !viewImage || !viewImage.hasAttribute( 'src' ) || !conversionApi.consumable.test( viewImage, { name: true } ) ) {
 			return;
 		}
 
 		// Convert view image to model image.
-		const modelImage = conversionApi.convertItem( viewImage, consumable, data );
+		const conversionResult = conversionApi.convertItem( viewImage, data.modelCursor );
 
-		// Convert rest of figure element's children, but in the context of model image, because those converted
-		// children will be added as model image children.
-		data.context.push( modelImage );
+		// Get image element from conversion result.
+		const modelImage = first( conversionResult.modelRange.getItems() );
 
-		const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
+		// When image wasn't successfully converted then finish conversion.
+		if ( !modelImage ) {
+			return;
+		}
 
-		data.context.pop();
+		// Convert rest of the figure element's children as an image children.
+		conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelImage ) );
 
-		// Add converted children to model image.
-		modelWriter.insert( ModelPosition.createAt( modelImage ), modelChildren );
+		// Set image range as conversion result.
+		data.modelRange = conversionResult.modelRange;
 
-		// Set model image as conversion result.
-		data.output = modelImage;
-	};
-}
-
-/**
- * Creates the image attribute converter for provided model conversion dispatchers.
- *
- * @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
- * @param {String} attributeName
- * @param {Function} [converter] Custom converter for the attribute - default one converts attribute from model `image` element
- * to the same attribute in `img` in the view.
- */
-export function createImageAttributeConverter( dispatchers, attributeName, converter = modelToViewAttributeConverter ) {
-	for ( const dispatcher of dispatchers ) {
-		dispatcher.on( `addAttribute:${ attributeName }:image`, converter() );
-		dispatcher.on( `changeAttribute:${ attributeName }:image`, converter() );
-		dispatcher.on( `removeAttribute:${ attributeName }:image`, converter() );
+		// Continue conversion where image conversion ends.
+		data.modelCursor = conversionResult.modelCursor;
 	}
 }
 
@@ -86,237 +71,64 @@ export function createImageAttributeConverter( dispatchers, attributeName, conve
  * @return {Function}
  */
 export function srcsetAttributeConverter() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const parts = evt.name.split( ':' );
-		const consumableType = parts[ 0 ] + ':' + parts[ 1 ];
-		const modelImage = data.item;
+	return dispatcher => {
+		dispatcher.on( 'attribute:srcset:image', converter );
+	};
 
-		if ( !consumable.consume( modelImage, consumableType ) ) {
+	function converter( evt, data, conversionApi ) {
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
 			return;
 		}
 
-		const figure = conversionApi.mapper.toViewElement( modelImage );
+		const writer = conversionApi.writer;
+		const figure = conversionApi.mapper.toViewElement( data.item );
 		const img = figure.getChild( 0 );
-		const type = parts[ 0 ];
 
-		if ( type == 'removeAttribute' ) {
+		if ( data.attributeNewValue === null ) {
 			const srcset = data.attributeOldValue;
 
 			if ( srcset.data ) {
-				img.removeAttribute( 'srcset' );
-				img.removeAttribute( 'sizes' );
+				writer.removeAttribute( 'srcset', img );
+				writer.removeAttribute( 'sizes', img );
 
 				if ( srcset.width ) {
-					img.removeAttribute( 'width' );
+					writer.removeAttribute( 'width', img );
 				}
 			}
 		} else {
 			const srcset = data.attributeNewValue;
 
 			if ( srcset.data ) {
-				img.setAttribute( 'srcset', srcset.data );
+				writer.setAttribute( 'srcset', srcset.data, img );
 				// Always outputting `100vw`. See https://github.com/ckeditor/ckeditor5-image/issues/2.
-				img.setAttribute( 'sizes', '100vw' );
+				writer.setAttribute( 'sizes', '100vw', img );
 
 				if ( srcset.width ) {
-					img.setAttribute( 'width', srcset.width );
+					writer.setAttribute( 'width', srcset.width, img );
 				}
 			}
 		}
-	};
+	}
 }
 
-// Returns model to view image converter converting given attribute, and adding it to `img` element nested inside `figure` element.
-//
-// @private
-function modelToViewAttributeConverter() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const parts = evt.name.split( ':' );
-		const consumableType = parts[ 0 ] + ':' + parts[ 1 ];
-		const modelImage = data.item;
+export function modelToViewAttributeConverter( attributeKey ) {
+	return dispatcher => {
+		dispatcher.on( `attribute:${ attributeKey }:image`, converter );
+	};
 
-		if ( !consumable.consume( modelImage, consumableType ) ) {
+	function converter( evt, data, conversionApi ) {
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
 			return;
 		}
 
-		const figure = conversionApi.mapper.toViewElement( modelImage );
+		const viewWriter = conversionApi.writer;
+		const figure = conversionApi.mapper.toViewElement( data.item );
 		const img = figure.getChild( 0 );
-		const type = parts[ 0 ];
 
-		if ( type == 'removeAttribute' ) {
-			img.removeAttribute( data.attributeKey );
+		if ( data.attributeNewValue !== null ) {
+			viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, img );
 		} else {
-			img.setAttribute( data.attributeKey, data.attributeNewValue );
-		}
-	};
-}
-
-// Holds all images that were converted for autohoisting.
-const autohoistedImages = new WeakSet();
-
-/**
- * A converter which converts `<img>` {@link module:engine/view/element~Element view elements} that can be hoisted.
- *
- * If an `<img>` view element has not been converted, this converter checks if that element could be converted in any
- * context "above". If it could, the converter converts the `<img>` element even though it is not allowed in the current
- * context and marks it to be autohoisted. Then {@link module:image/image/converters~hoistImageThroughElement another converter}
- * moves the converted element to the correct location.
- */
-export function convertHoistableImage( evt, data, consumable, conversionApi ) {
-	const img = data.input;
-
-	// If the image has not been consumed (converted)...
-	if ( !consumable.test( img, { name: true, attribute: [ 'src' ] } ) ) {
-		return;
-	}
-	// At this point the image has not been converted because it was not allowed by schema. It might be in wrong
-	// context or missing an attribute, but above we already checked whether the image has mandatory src attribute.
-
-	// If the image would be allowed if it was in one of its ancestors...
-	const allowedContext = _findAllowedContext( { name: 'image', attributes: [ 'src' ] }, data.context, conversionApi.schema );
-
-	if ( !allowedContext ) {
-		return;
-	}
-
-	// Convert it in that context...
-	const newData = Object.assign( {}, data );
-	newData.context = allowedContext;
-
-	data.output = conversionApi.convertItem( img, consumable, newData );
-
-	// And mark that image to be hoisted.
-	autohoistedImages.add( data.output );
-}
-
-// Basing on passed `context`, searches for "closest" context in which model element represented by `modelData`
-// would be allowed by `schema`.
-//
-// @private
-// @param {Object} modelData Object describing model element to check. Has two properties: `name` with model element name
-// and `attributes` with keys of attributes of that model element.
-// @param {Array} context Context in which original conversion was supposed to take place.
-// @param {module:engine/model/schema~Schema} schema Schema to check with.
-// @returns {Array|null} Context in which described model element would be allowed by `schema` or `null` if such context
-// could not been found.
-function _findAllowedContext( modelData, context, schema ) {
-	// Copy context array so we won't modify original array.
-	context = context.slice();
-
-	// Prepare schema query to check with schema.
-	// Since `inside` property is passed as reference to `context` variable, we don't need to modify `schemaQuery`.
-	const schemaQuery = {
-		name: modelData.name,
-		attributes: modelData.attributes,
-		inside: context
-	};
-
-	// Try out all possible contexts.
-	while ( context.length && !schema.check( schemaQuery ) ) {
-		const parent = context.pop();
-		const parentName = typeof parent === 'string' ? parent : parent.name;
-
-		// Do not try to autohoist "above" limiting element.
-		if ( schema.limits.has( parentName ) ) {
-			return null;
-		}
-	}
-
-	// If `context` has any items it means that image is allowed in that context. Return that context.
-	// If `context` has no items it means that image was not allowed in any of possible contexts. Return `null`.
-	return context.length ? context : null;
-}
-
-/**
- * A converter which hoists `<image>` {@link module:engine/model/element~Element model elements} to allowed context.
- *
- * It looks through all children of the converted {@link module:engine/view/element~Element view element} if it
- * was converted to a model element. It breaks the model element if an `<image>` to-be-hoisted is found.
- *
- *		<div><paragraph>x<image src="foo.jpg"></image>x</paragraph></div> ->
- *		<div><paragraph>x</paragraph></div><image src="foo.jpg"></image><div><paragraph>x</paragraph></div>
- *
- * This works deeply, as shown in the example. This converter added for the `<paragraph>` element will break the `<paragraph>`
- *  element and pass the {@link module:engine/model/documentfragment~DocumentFragment document fragment} in `data.output`.
- *  Then, the `<div>` will be handled by this converter and will be once again broken to hoist the `<image>` up to the root.
- *
- * **Note:** This converter should be executed only after the view element has already been converted, which means that
- * `data.output` for that view element should be already generated when this converter is fired.
- */
-export function hoistImageThroughElement( evt, data ) {
-	// If this element has been properly converted...
-	if ( !data.output ) {
-		return;
-	}
-
-	// And it is an element...
-	// (If it is document fragment autohoisting does not have to break anything anyway.)
-	// (And if it is text there are no children here.)
-	if ( !data.output.is( 'element' ) ) {
-		return;
-	}
-
-	// This will hold newly generated output. At the beginning it is only the original element.
-	const newOutput = [];
-
-	// Check if any of its children is to be hoisted...
-	// Start from the last child - it is easier to break that way.
-	for ( let i = data.output.childCount - 1; i >= 0; i-- ) {
-		const child = data.output.getChild( i );
-
-		if ( autohoistedImages.has( child ) ) {
-			// Break autohoisted element's parent:
-			// <parent>{ left-children... }<authoistedElement />{ right-children... }</parent>   --->
-			// <parent>{ left-children... }</parent><autohoistedElement /><parent>{ right-children... }</parent>
-			//
-			// or
-			//
-			// <parent>{ left-children... }<autohoistedElement /></parent> --->
-			// <parent>{ left-children... }</parent><autohoistedElement />
-			//
-			// or
-			//
-			// <parent><autohoistedElement />{ right-children... }</parent> --->
-			// <autohoistedElement /><parent>{ right-children... }</parent>
-			//
-			// or
-			//
-			// <parent><autohoistedElement /></parent> ---> <autohoistedElement />
-
-			// Check how many right-children there are.
-			const rightChildrenCount = data.output.childCount - i - 1;
-			let rightParent = null;
-
-			// If there are any right-children, clone the prent element and insert those children there.
-			if ( rightChildrenCount > 0 ) {
-				rightParent = data.output.clone( false );
-				rightParent.appendChildren( data.output.removeChildren( i + 1, rightChildrenCount ) );
-			}
-
-			// Remove the autohoisted element from its parent.
-			child.remove();
-
-			// Break "leading" `data.output` in `newOutput` into one or more pieces:
-			// Remove "leading" `data.output` (note that `data.output` is always first item in `newOutput`).
-			newOutput.shift();
-
-			// Add the newly created parent of the right-children at the beginning.
-			if ( rightParent ) {
-				newOutput.unshift( rightParent );
-			}
-
-			// Add autohoisted element at the beginning.
-			newOutput.unshift( child );
-
-			// Add `data.output` at the beginning, if there is anything left in it.
-			if ( data.output.childCount > 0 ) {
-				newOutput.unshift( data.output );
-			}
+			viewWriter.removeAttribute( data.attributeKey, img );
 		}
 	}
-
-	// If the output has changed pass it further.
-	if ( newOutput.length ) {
-		data.output = new ModelDocumentFragment( newOutput );
-	}
 }

+ 122 - 0
packages/ckeditor5-image/src/image/imageediting.js

@@ -0,0 +1,122 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/image/imageediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import {
+	viewFigureToModel,
+	modelToViewAttributeConverter,
+	srcsetAttributeConverter
+} from './converters';
+
+import { toImageWidget } from './utils';
+
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import { upcastElementToElement, upcastAttributeToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
+
+/**
+ * The image engine plugin.
+ * Registers `<image>` as a block element in the document schema, and allows `alt`, `src` and `srcset` attributes.
+ * Registers converters for editing and data pipelines.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+		const t = editor.t;
+		const conversion = editor.conversion;
+
+		// Configure schema.
+		schema.register( 'image', {
+			isObject: true,
+			isBlock: true,
+			allowWhere: '$block',
+			allowAttributes: [ 'alt', 'src', 'srcset' ]
+		} );
+
+		conversion.for( 'dataDowncast' ).add( downcastElementToElement( {
+			model: 'image',
+			view: ( modelElement, viewWriter ) => createImageViewElement( viewWriter )
+		} ) );
+
+		conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
+			model: 'image',
+			view: ( modelElement, viewWriter ) => toImageWidget( createImageViewElement( viewWriter ), viewWriter, t( 'image widget' ) )
+		} ) );
+
+		conversion.for( 'downcast' )
+			.add( modelToViewAttributeConverter( 'src' ) )
+			.add( modelToViewAttributeConverter( 'alt' ) )
+			.add( srcsetAttributeConverter() );
+
+		conversion.for( 'upcast' )
+			.add( upcastElementToElement( {
+				view: {
+					name: 'img',
+					attribute: {
+						src: true
+					}
+				},
+				model: ( viewImage, modelWriter ) => modelWriter.createElement( 'image', { src: viewImage.getAttribute( 'src' ) } )
+			} ) )
+			.add( upcastAttributeToAttribute( {
+				view: {
+					name: 'img',
+					key: 'alt'
+				},
+				model: 'alt'
+			} ) )
+			.add( upcastAttributeToAttribute( {
+				view: {
+					name: 'img',
+					key: 'srcset'
+				},
+				model: {
+					key: 'srcset',
+					value: viewImage => {
+						const value = {
+							data: viewImage.getAttribute( 'srcset' )
+						};
+
+						if ( viewImage.hasAttribute( 'width' ) ) {
+							value.width = viewImage.getAttribute( 'width' );
+						}
+
+						return value;
+					}
+				}
+			} ) )
+			.add( viewFigureToModel() );
+	}
+}
+
+// Creates a view element representing the image.
+//
+//		<figure class="image"><img></img></figure>
+//
+// Note that `alt` and `src` attributes are converted separately, so they are not included.
+//
+// @private
+// @param {module:engine/view/writer~Writer} writer
+// @return {module:engine/view/containerelement~ContainerElement}
+export function createImageViewElement( writer ) {
+	const emptyElement = writer.createEmptyElement( 'img' );
+	const figure = writer.createContainerElement( 'figure', { class: 'image' } );
+
+	writer.insert( ViewPosition.createAt( figure ), emptyElement );
+
+	return figure;
+}

+ 0 - 116
packages/ckeditor5-image/src/image/imageengine.js

@@ -1,116 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module image/image/imageengine
- */
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
-import {
-	viewFigureToModel,
-	createImageAttributeConverter,
-	convertHoistableImage,
-	hoistImageThroughElement,
-	srcsetAttributeConverter
-} from './converters';
-import { toImageWidget } from './utils';
-import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
-import ViewEmptyElement from '@ckeditor/ckeditor5-engine/src/view/emptyelement';
-
-/**
- * The image engine plugin.
- * Registers `<image>` as a block element in the document schema, and allows `alt`, `src` and `srcset` attributes.
- * Registers converters for editing and data pipelines.
- *
- * @extends module:core/plugin~Plugin
- */
-export default class ImageEngine extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const doc = editor.document;
-		const schema = doc.schema;
-		const data = editor.data;
-		const editing = editor.editing;
-		const t = editor.t;
-
-		// Configure schema.
-		schema.registerItem( 'image' );
-		schema.requireAttributes( 'image', [ 'src' ] );
-		schema.allow( { name: 'image', attributes: [ 'alt', 'src', 'srcset' ], inside: '$root' } );
-		schema.objects.add( 'image' );
-
-		// Build converter from model to view for data pipeline.
-		buildModelConverter().for( data.modelToView )
-			.fromElement( 'image' )
-			.toElement( () => createImageViewElement() );
-
-		// Build converter from model to view for editing pipeline.
-		buildModelConverter().for( editing.modelToView )
-			.fromElement( 'image' )
-			.toElement( () => toImageWidget( createImageViewElement(), t( 'image widget' ) ) );
-
-		createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'src' );
-		createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'alt' );
-
-		// Convert `srcset` attribute changes and add or remove `sizes` attribute when necessary.
-		createImageAttributeConverter( [ editing.modelToView, data.modelToView ], 'srcset', srcsetAttributeConverter );
-
-		// Build converter for view img element to model image element.
-		buildViewConverter().for( data.viewToModel )
-			.from( { name: 'img', attribute: { src: /./ } } )
-			.toElement( viewImage => new ModelElement( 'image', { src: viewImage.getAttribute( 'src' ) } ) );
-
-		data.viewToModel.on( 'element:img', convertHoistableImage, { priority: 'low' } );
-		data.viewToModel.on( 'element', hoistImageThroughElement, { priority: 'low' } );
-
-		// Build converter for alt attribute.
-		// Note that by default attribute converters are added with `low` priority.
-		// This converter will be thus fired after `convertHoistableImage` converter.
-		buildViewConverter().for( data.viewToModel )
-			.from( { name: 'img', attribute: { alt: /./ } } )
-			.consuming( { attribute: [ 'alt' ] } )
-			.toAttribute( viewImage => ( { key: 'alt', value: viewImage.getAttribute( 'alt' ) } ) );
-
-		// Build converter for srcset attribute.
-		buildViewConverter().for( data.viewToModel )
-			.from( { name: 'img', attribute: { srcset: /./ } } )
-			.consuming( { attribute: [ 'srcset' ] } )
-			.toAttribute( viewImage => {
-				const value = {
-					data: viewImage.getAttribute( 'srcset' )
-				};
-
-				if ( viewImage.hasAttribute( 'width' ) ) {
-					value.width = viewImage.getAttribute( 'width' );
-				}
-
-				return {
-					key: 'srcset',
-					value
-				};
-			} );
-
-		// Converter for figure element from view to model.
-		data.viewToModel.on( 'element:figure', viewFigureToModel() );
-	}
-}
-
-// Creates a view element representing the image.
-//
-//		<figure class="image"><img></img></figure>
-//
-// Note that `alt` and `src` attributes are converted separately, so they are not included.
-//
-// @private
-// @return {module:engine/view/containerelement~ContainerElement}
-export function createImageViewElement() {
-	return new ViewContainerElement( 'figure', { class: 'image' }, new ViewEmptyElement( 'img' ) );
-}

+ 8 - 4
packages/ckeditor5-image/src/image/ui/utils.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -20,7 +20,7 @@ import { isImageWidgetSelected } from '../utils';
 export function repositionContextualBalloon( editor ) {
 	const balloon = editor.plugins.get( 'ContextualBalloon' );
 
-	if ( isImageWidgetSelected( editor.editing.view.selection ) ) {
+	if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) {
 		const position = getBalloonPositionData( editor );
 
 		balloon.updatePosition( position );
@@ -40,10 +40,14 @@ export function getBalloonPositionData( editor ) {
 	const defaultPositions = BalloonPanelView.defaultPositions;
 
 	return {
-		target: editingView.domConverter.viewToDom( editingView.selection.getSelectedElement() ),
+		target: editingView.domConverter.viewToDom( editingView.document.selection.getSelectedElement() ),
 		positions: [
 			defaultPositions.northArrowSouth,
-			defaultPositions.southArrowNorth
+			defaultPositions.northArrowSouthWest,
+			defaultPositions.northArrowSouthEast,
+			defaultPositions.southArrowNorth,
+			defaultPositions.southArrowNorthWest,
+			defaultPositions.southArrowNorthEast
 		]
 	};
 }

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -14,17 +14,18 @@ const imageSymbol = Symbol( 'isImage' );
 
 /**
  * Converts a given {@link module:engine/view/element~Element} to an image widget:
- * * adds a {@link module:engine/view/element~Element#setCustomProperty custom property} allowing to recognize the image widget element,
+ * * adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the image widget element,
  * * calls the {@link module:widget/utils~toWidget toWidget} function with the proper element's label creator.
  *
  * @param {module:engine/view/element~Element} viewElement
+ * @param {module:engine/view/writer~Writer} writer Instance of view writer.
  * @param {String} label Element's label. It will be concatenated with the image `alt` attribute if one is present.
  * @returns {module:engine/view/element~Element}
  */
-export function toImageWidget( viewElement, label ) {
-	viewElement.setCustomProperty( imageSymbol, true );
+export function toImageWidget( viewElement, writer, label ) {
+	writer.setCustomProperty( imageSymbol, true, viewElement );
 
-	return toWidget( viewElement, { label: labelCreator } );
+	return toWidget( viewElement, writer, { label: labelCreator } );
 
 	function labelCreator() {
 		const imgElement = viewElement.getChild( 0 );

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,8 +8,9 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageCaptionEngine from './imagecaption/imagecaptionengine';
-import '../theme/imagecaption/theme.scss';
+import ImageCaptionEditing from './imagecaption/imagecaptionediting';
+
+import '../theme/imagecaption.css';
 
 /**
  * The image caption plugin.
@@ -21,7 +22,7 @@ export default class ImageCaption extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageCaptionEngine ];
+		return [ ImageCaptionEditing ];
 	}
 
 	/**

+ 293 - 0
packages/ckeditor5-image/src/imagecaption/imagecaptionediting.js

@@ -0,0 +1,293 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagecaption/imagecaptionediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+import { isImage } from '../image/utils';
+import {
+	captionElementCreator,
+	getCaptionFromImage,
+	matchImageCaption
+} from './utils';
+
+/**
+ * The image caption engine plugin.
+ *
+ * It registers proper converters. It takes care of adding a caption element if the image without it is inserted
+ * to the model document.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageCaptionEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const view = editor.editing.view;
+		const schema = editor.model.schema;
+		const data = editor.data;
+		const editing = editor.editing;
+		const t = editor.t;
+
+		/**
+		 * Last selected caption editable.
+		 * It is used for hiding the editable when it is empty and the image widget is no longer selected.
+		 *
+		 * @private
+		 * @member {module:engine/view/editableelement~EditableElement} #_lastSelectedCaption
+		 */
+
+		// Schema configuration.
+		schema.register( 'caption', {
+			allowIn: 'image',
+			allowContentOf: '$block',
+			isLimit: true
+		} );
+
+		// Add caption element to each image inserted without it.
+		editor.model.document.registerPostFixer( writer => this._insertMissingModelCaptionElement( writer ) );
+
+		// View to model converter for the data pipeline.
+		editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+			view: matchImageCaption,
+			model: 'caption'
+		} ) );
+
+		// Model to view converter for the data pipeline.
+		const createCaptionForData = writer => writer.createContainerElement( 'figcaption' );
+		data.downcastDispatcher.on( 'insert:caption', captionModelToView( createCaptionForData, false ) );
+
+		// Model to view converter for the editing pipeline.
+		const createCaptionForEditing = captionElementCreator( view, t( 'Enter image caption' ) );
+		editing.downcastDispatcher.on( 'insert:caption', captionModelToView( createCaptionForEditing ) );
+
+		// Always show caption in view when something is inserted in model.
+		editing.downcastDispatcher.on(
+			'insert',
+			this._fixCaptionVisibility( data => data.item ),
+			{ priority: 'high' }
+		);
+
+		// Hide caption when everything is removed from it.
+		editing.downcastDispatcher.on( 'remove', this._fixCaptionVisibility( data => data.position.parent ), { priority: 'high' } );
+
+		// Update caption visibility on view in post fixer.
+		view.document.registerPostFixer( writer => this._updateCaptionVisibility( writer ) );
+	}
+
+	/**
+	 * Updates the view before each rendering, making sure that empty captions (so unnecessary ones) are hidden
+	 * and then visible when the image is selected.
+	 *
+	 * @private
+	 * @param {module:engine/view/writer~Writer} viewWriter
+	 * @returns {Boolean} Returns `true` when view is updated.
+	 */
+	_updateCaptionVisibility( viewWriter ) {
+		const mapper = this.editor.editing.mapper;
+		const lastCaption = this._lastSelectedCaption;
+		let viewCaption;
+
+		// If whole image is selected.
+		const modelSelection = this.editor.model.document.selection;
+		const selectedElement = modelSelection.getSelectedElement();
+
+		if ( selectedElement && selectedElement.is( 'image' ) ) {
+			const modelCaption = getCaptionFromImage( selectedElement );
+			viewCaption = mapper.toViewElement( modelCaption );
+		}
+
+		// If selection is placed inside caption.
+		const position = modelSelection.getFirstPosition();
+		const modelCaption = getParentCaption( position.parent );
+
+		if ( modelCaption ) {
+			viewCaption = mapper.toViewElement( modelCaption );
+		}
+
+		// Is currently any caption selected?
+		if ( viewCaption ) {
+			// Was any caption selected before?
+			if ( lastCaption ) {
+				// Same caption as before?
+				if ( lastCaption === viewCaption ) {
+					return showCaption( viewCaption, viewWriter );
+				} else {
+					hideCaptionIfEmpty( lastCaption, viewWriter );
+					this._lastSelectedCaption = viewCaption;
+
+					return showCaption( viewCaption, viewWriter );
+				}
+			} else {
+				this._lastSelectedCaption = viewCaption;
+				return showCaption( viewCaption, viewWriter );
+			}
+		} else {
+			// Was any caption selected before?
+			if ( lastCaption ) {
+				const viewModified = hideCaptionIfEmpty( lastCaption, viewWriter );
+				this._lastSelectedCaption = null;
+
+				return viewModified;
+			} else {
+				return false;
+			}
+		}
+	}
+
+	/**
+	 * Returns converter that fixes caption visibility during the model-to-view conversion.
+	 * Checks if the changed node is placed inside the caption element and fixes its visibility in the view.
+	 *
+	 * @private
+	 * @param {Function} nodeFinder
+	 * @returns {Function}
+	 */
+	_fixCaptionVisibility( nodeFinder ) {
+		return ( evt, data, conversionApi ) => {
+			const node = nodeFinder( data );
+			const modelCaption = getParentCaption( node );
+			const mapper = this.editor.editing.mapper;
+			const viewWriter = conversionApi.writer;
+
+			if ( modelCaption ) {
+				const viewCaption = mapper.toViewElement( modelCaption );
+
+				if ( viewCaption ) {
+					if ( modelCaption.childCount ) {
+						viewWriter.removeClass( 'ck-hidden', viewCaption );
+					} else {
+						viewWriter.addClass( 'ck-hidden', 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
+	 * @param {module:engine/model/writer~Writer} writer Writer to make changes with.
+	 * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
+	 */
+	_insertMissingModelCaptionElement( writer ) {
+		const model = this.editor.model;
+		const changes = model.document.differ.getChanges();
+
+		for ( const entry of changes ) {
+			if ( entry.type == 'insert' && entry.name == 'image' ) {
+				const item = entry.position.nodeAfter;
+
+				if ( !getCaptionFromImage( item ) ) {
+					writer.appendElement( 'caption', item );
+
+					return true;
+				}
+			}
+		}
+	}
+}
+
+// Creates a converter that converts image caption model element to view element.
+//
+// @private
+// @param {Function} elementCreator
+// @param {Boolean} [hide=true] When set to `false` view element will not be inserted when it's empty.
+// @return {Function}
+function captionModelToView( elementCreator, hide = true ) {
+	return ( evt, data, conversionApi ) => {
+		const captionElement = data.item;
+
+		// Return if element shouldn't be present when empty.
+		if ( !captionElement.childCount && !hide ) {
+			return;
+		}
+
+		if ( isImage( captionElement.parent ) ) {
+			if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
+				return;
+			}
+
+			const viewImage = conversionApi.mapper.toViewElement( data.range.start.parent );
+			const viewCaption = elementCreator( conversionApi.writer );
+			const viewWriter = conversionApi.writer;
+
+			// Hide if empty.
+			if ( !captionElement.childCount ) {
+				viewWriter.addClass( 'ck-hidden', viewCaption );
+			}
+
+			insertViewCaptionAndBind( viewCaption, data.item, viewImage, conversionApi );
+		}
+	};
+}
+
+// Inserts `viewCaption` at the end of `viewImage` and binds it to `modelCaption`.
+//
+// @private
+// @param {module:engine/view/containerelement~ContainerElement} viewCaption
+// @param {module:engine/model/element~Element} modelCaption
+// @param {module:engine/view/containerelement~ContainerElement} viewImage
+// @param {Object} conversionApi
+function insertViewCaptionAndBind( viewCaption, modelCaption, viewImage, conversionApi ) {
+	const viewPosition = ViewPosition.createAt( viewImage, 'end' );
+
+	conversionApi.writer.insert( viewPosition, viewCaption );
+	conversionApi.mapper.bindElements( modelCaption, viewCaption );
+}
+
+// Checks if the provided node or one of its ancestors is a caption element, and returns it.
+//
+// @private
+// @param {module:engine/model/node~Node} node
+// @returns {module:engine/model/element~Element|null}
+function getParentCaption( node ) {
+	const ancestors = node.getAncestors( { includeSelf: true } );
+	const caption = ancestors.find( ancestor => ancestor.name == 'caption' );
+
+	if ( caption && caption.parent && caption.parent.name == 'image' ) {
+		return caption;
+	}
+
+	return null;
+}
+
+// Hides given caption in the view if it's empty.
+//
+// @private
+// @param {module:engine/view/containerelement~ContainerElement} caption
+// @param {module:engine/view/writer~Writer} viewWriter
+// @returns {Boolean} Returns `true` if view was modified.
+function hideCaptionIfEmpty( caption, viewWriter ) {
+	if ( !caption.childCount && !caption.hasClass( 'ck-hidden' ) ) {
+		viewWriter.addClass( 'ck-hidden', caption );
+		return true;
+	}
+
+	return false;
+}
+
+// Shows the caption
+//
+// @private
+// @param {module:engine/view/containerelement~ContainerElement} caption
+// @param {module:engine/view/writer~Writer} viewWriter
+// @returns {Boolean} Returns `true` if view was modified.
+function showCaption( caption, viewWriter ) {
+	if ( caption.hasClass( 'ck-hidden' ) ) {
+		viewWriter.removeClass( 'ck-hidden', caption );
+		return true;
+	}
+
+	return false;
+}

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

@@ -1,249 +0,0 @@
-/**
- * @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 viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
-import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
-import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
-import { isImage } from '../image/utils';
-import {
-	captionElementCreator,
-	getCaptionFromImage,
-	matchImageCaption
-} from './utils';
-
-/**
- * The image caption engine plugin.
- *
- * It registers proper converters. It takes care of adding a caption element if the image without it is inserted
- * to the 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;
-		const t = editor.t;
-
-		/**
-		 * Last selected caption editable.
-		 * It is used for hiding the editable when it is empty and the image widget is no longer selected.
-		 *
-		 * @private
-		 * @member {module:engine/view/editableelement~EditableElement} #_lastSelectedCaption
-		 */
-
-		/**
-		 * A function used to create the editable caption element in the editing view.
-		 *
-		 * @private
-		 * @member {Function}
-		 */
-		this._createCaption = captionElementCreator( viewDocument, t( 'Enter image caption' ) );
-
-		// Schema configuration.
-		schema.registerItem( 'caption', '$block' );
-		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', insertMissingModelCaptionElement );
-
-		// View to model converter for the data pipeline.
-		buildViewConverter()
-			.for( data.viewToModel )
-			.from( matchImageCaption )
-			.toElement( 'caption' );
-
-		// Model to view converter for the data pipeline.
-		data.modelToView.on( 'insert:caption', captionModelToView( new ViewContainerElement( 'figcaption' ), false ) );
-
-		// Model to view converter for the editing pipeline.
-		editing.modelToView.on( 'insert:caption', captionModelToView( this._createCaption ) );
-
-		// Always show caption in view when something is inserted in model.
-		editing.modelToView.on( 'insert', ( evt, data ) => this._fixCaptionVisibility( data.item ), { priority: 'high' } );
-
-		// Hide caption when everything is removed from it.
-		editing.modelToView.on( 'remove', ( evt, data ) => this._fixCaptionVisibility( data.sourcePosition.parent ), { priority: 'high' } );
-
-		// Update view before each rendering.
-		this.listenTo( viewDocument, 'render', () => this._updateCaptionVisibility(), { priority: 'high' } );
-	}
-
-	/**
-	 * Updates the view before each rendering, making sure that empty captions (so unnecessary ones) are hidden
-	 * and then visible when the image is selected.
-	 *
-	 * @private
-	 */
-	_updateCaptionVisibility() {
-		const mapper = this.editor.editing.mapper;
-		let viewCaption;
-
-		// Hide last selected caption if have no child elements.
-		if ( this._lastSelectedCaption && !this._lastSelectedCaption.childCount ) {
-			this._lastSelectedCaption.addClass( 'ck-hidden' );
-		}
-
-		// If whole image is selected.
-		const modelSelection = this.editor.document.selection;
-		const selectedElement = modelSelection.getSelectedElement();
-
-		if ( selectedElement && selectedElement.is( 'image' ) ) {
-			const modelCaption = getCaptionFromImage( selectedElement );
-			viewCaption = mapper.toViewElement( modelCaption );
-		}
-
-		// If selection is placed inside caption.
-		const position = modelSelection.getFirstPosition();
-		const modelCaption = getParentCaption( position.parent );
-
-		if ( modelCaption ) {
-			viewCaption = mapper.toViewElement( modelCaption );
-		}
-
-		if ( viewCaption ) {
-			viewCaption.removeClass( 'ck-hidden' );
-			this._lastSelectedCaption = viewCaption;
-		}
-	}
-
-	/**
-	 * Fixes caption visibility during the model-to-view conversion.
-	 * Checks if the changed node is placed inside the caption element and fixes its visibility in the view.
-	 *
-	 * @private
-	 * @param {module:engine/model/node~Node} node
-	 */
-	_fixCaptionVisibility( node ) {
-		const modelCaption = getParentCaption( node );
-		const mapper = this.editor.editing.mapper;
-
-		if ( modelCaption ) {
-			const viewCaption = mapper.toViewElement( modelCaption );
-
-			if ( viewCaption ) {
-				if ( modelCaption.childCount ) {
-					viewCaption.removeClass( 'ck-hidden' );
-				} else {
-					viewCaption.addClass( 'ck-hidden' );
-				}
-			}
-		}
-	}
-}
-
-// 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 insertMissingModelCaptionElement( evt, changeType, data, batch ) {
-	if ( changeType !== 'insert' ) {
-		return;
-	}
-
-	const walker = new ModelTreeWalker( {
-		boundaries: data.range,
-		ignoreElementEnd: true
-	} );
-
-	for ( const value of walker ) {
-		const item = value.item;
-
-		if ( value.type == 'elementStart' && isImage( item ) && !getCaptionFromImage( item ) ) {
-			batch.document.enqueueChanges( () => {
-				// Make sure that the image does not have caption already.
-				// https://github.com/ckeditor/ckeditor5-image/issues/78
-				if ( !getCaptionFromImage( item ) ) {
-					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
-// @param {Boolean} [hide=true] When set to `false` view element will not be inserted when it's empty.
-// @return {Function}
-function captionModelToView( elementCreator, hide = true ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const captionElement = data.item;
-
-		// Return if element shouldn't be present when empty.
-		if ( !captionElement.childCount && !hide ) {
-			return;
-		}
-
-		if ( isImage( captionElement.parent ) ) {
-			if ( !consumable.consume( data.item, 'insert' ) ) {
-				return;
-			}
-
-			const viewImage = conversionApi.mapper.toViewElement( data.range.start.parent );
-			const viewCaption = ( elementCreator instanceof ViewElement ) ?
-				elementCreator.clone( true ) :
-				elementCreator();
-
-			// Hide if empty.
-			if ( !captionElement.childCount ) {
-				viewCaption.addClass( 'ck-hidden' );
-			}
-
-			insertViewCaptionAndBind( viewCaption, data.item, viewImage, conversionApi.mapper );
-		}
-	};
-}
-
-// Inserts `viewCaption` at the end of `viewImage` and binds it to `modelCaption`.
-//
-// @private
-// @param {module:engine/view/containerelement~ContainerElement} viewCaption
-// @param {module:engine/model/element~Element} modelCaption
-// @param {module:engine/view/containerelement~ContainerElement} viewImage
-// @param {module:engine/conversion/mapper~Mapper} mapper
-function insertViewCaptionAndBind( viewCaption, modelCaption, viewImage, mapper ) {
-	const viewPosition = ViewPosition.createAt( viewImage, 'end' );
-
-	viewWriter.insert( viewPosition, viewCaption );
-	mapper.bindElements( modelCaption, viewCaption );
-}
-
-/**
- * Checks if the provided node or one of its ancestors is a caption element, and returns it.
- *
- * @param {module:engine/model/node~Node} node
- * @returns {module:engine/model/element~Element|null}
- */
-function getParentCaption( node ) {
-	const ancestors = node.getAncestors( { includeSelf: true } );
-	const caption = ancestors.find( ancestor => ancestor.name == 'caption' );
-
-	if ( caption && caption.parent && caption.parent.name == 'image' ) {
-		return caption;
-	}
-
-	return null;
-}

+ 8 - 10
packages/ckeditor5-image/src/imagecaption/utils.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,7 +8,6 @@
  */
 
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import { attachPlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';
 import { toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
 
@@ -17,18 +16,17 @@ const captionSymbol = Symbol( 'imageCaption' );
 /**
  * Returns a function that creates a caption editable element for the given {@link module:engine/view/document~Document}.
  *
- * @param {module:engine/view/document~Document} viewDocument
+ * @param {module:engine/view/view~View} view
  * @param {String} placeholderText The text to be displayed when the caption is empty.
  * @return {Function}
  */
-export function captionElementCreator( viewDocument, placeholderText ) {
-	return () => {
-		const editable = new ViewEditableElement( 'figcaption' );
-		editable.document = viewDocument;
-		editable.setCustomProperty( captionSymbol, true );
-		attachPlaceholder( editable, placeholderText );
+export function captionElementCreator( view, placeholderText ) {
+	return writer => {
+		const editable = writer.createEditableElement( 'figcaption' );
+		writer.setCustomProperty( captionSymbol, true, editable );
+		attachPlaceholder( view, editable, placeholderText );
 
-		return toWidgetEditable( editable );
+		return toWidgetEditable( editable, writer );
 	};
 }
 

+ 21 - 61
packages/ckeditor5-image/src/imagestyle.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,13 +8,14 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageStyleEngine from './imagestyle/imagestyleengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import ImageStyleEditing from './imagestyle/imagestyleediting';
+import ImageStyleUI from './imagestyle/imagestyleui';
 
 /**
  * The image style plugin.
  *
- * Uses the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine}.
+ * It loads the {@link module:image/imagestyle/imagestyleediting~ImageStyleEditing}
+ * and {@link module:image/imagestyle/imagestyleui~ImageStyleUI} plugins.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -23,7 +24,7 @@ export default class ImageStyle extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageStyleEngine ];
+		return [ ImageStyleEditing, ImageStyleUI ];
 	}
 
 	/**
@@ -32,56 +33,15 @@ export default class ImageStyle extends Plugin {
 	static get pluginName() {
 		return 'ImageStyle';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const styles = editor.plugins.get( ImageStyleEngine ).imageStyles;
-
-		for ( const style of styles ) {
-			this._createButton( style );
-		}
-	}
-
-	/**
-	 * Creates a button for each style and stores it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
-	 *
-	 * @private
-	 * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
-	 */
-	_createButton( style ) {
-		const editor = this.editor;
-		const command = editor.commands.get( style.name );
-
-		editor.ui.componentFactory.add( style.name, locale => {
-			const view = new ButtonView( locale );
-
-			view.set( {
-				label: style.title,
-				icon: style.icon,
-				tooltip: true
-			} );
-
-			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
-			view.bind( 'isOn' ).to( command, 'value' );
-
-			this.listenTo( view, 'execute', () => editor.execute( style.name ) );
-
-			return view;
-		} );
-	}
 }
 
 /**
  * Available image styles.
- * The option is used by the {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine} feature.
  *
  * The default value is:
  *
- * 		const imageConfig = {
- *			styles: [ 'imageStyleFull', 'imageStyleSide' ]
+ *		const imageConfig = {
+ *			styles: [ 'full', 'side' ]
  *		};
  *
  * which configures two default styles:
@@ -89,12 +49,12 @@ export default class ImageStyle extends Plugin {
  *  * the "full" style which doesn't apply any class, e.g. for images styled to span 100% width of the content,
  *  * the "side" style with the `.image-style-side` CSS class.
  *
- * See {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles} to learn more about default
+ * See {@link module:image/imagestyle/utils~defaultStyles} to learn more about default
  * styles provided by the image feature.
  *
- * The {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles default styles} can be customized,
+ * The {@link module:image/imagestyle/utils~defaultStyles default styles} can be customized,
  * e.g. to change the icon, title or CSS class of the style. The feature also provides several
- * {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultIcons default icons} to chose from.
+ * {@link module:image/imagestyle/utils~defaultIcons default icons} to chose from.
  *
  *		import customIcon from 'custom-icon.svg';
  *
@@ -104,10 +64,10 @@ export default class ImageStyle extends Plugin {
  *			styles: [
  *				// This will only customize the icon of the "full" style.
  *				// Note: 'right' is one of default icons provided by the feature.
- *				{ name: 'imageStyleFull', icon: 'right' },
+ *				{ name: 'full', icon: 'right' },
  *
  *				// This will customize the icon, title and CSS class of the default "side" style.
- *				{ name: 'imageStyleSide', icon: customIcon, title: 'My side style', class: 'custom-side-image' }
+ *				{ name: 'side', icon: customIcon, title: 'My side style', class: 'custom-side-image' }
  *			]
  *		};
  *
@@ -127,23 +87,23 @@ export default class ImageStyle extends Plugin {
  *			]
  *		};
  *
- * Note: Setting `title` to one of {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine#localizedDefaultStylesTitles}
+ * Note: Setting `title` to one of {@link module:image/imagestyle/imagestyleui~ImageStyleUI#localizedDefaultStylesTitles}
  * will automatically translate it to the language of the editor.
  *
- * Read more about styling images in the {@glink features/image#Image-styles Image styles guide}.
+ * Read more about styling images in the {@glink features/image#image-styles Image styles guide}.
  *
  * The feature creates commands based on defined styles, so you can change the style of a selected image by executing
  * the following command:
  *
- *		editor.execute( 'imageStyleSide' );
+ *		editor.execute( 'imageStyle' { value: 'side' } );
  *
- * The features creates also buttons which execute the commands, so assuming that you use the
- * default image styles setting you can {@link module:image/image~ImageConfig#toolbar configure the image toolbar}
- * to contain these options:
+ * The feature creates also buttons which execute the command. So, assuming that you use the
+ * default image styles setting, you can {@link module:image/image~ImageConfig#toolbar configure the image toolbar}
+ * (or any other toolbar) to contain these options:
  *
  *		const imageConfig = {
- *			toolbar: [ 'imageStyleFull', 'imageStyleSide' ]
+ *			toolbar: [ 'imageStyle:full', 'imageStyle:side' ]
  *		};
  *
- * @member {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>} module:image/image~ImageConfig#styles
+ * @member {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} module:image/image~ImageConfig#styles
  */

+ 34 - 86
packages/ckeditor5-image/src/imagestyle/converters.js

@@ -1,41 +1,40 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
+import first from '@ckeditor/ckeditor5-utils/src/first';
+
 /**
  * @module image/imagestyle/converters
  */
 
-import { isImage } from '../image/utils';
-
 /**
  * Returns a converter for the `imageStyle` attribute. It can be used for adding, changing and removing the attribute.
  *
- * @param {Object} styles An object containing available styles. See {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}
+ * @param {Object} styles An object containing available styles. See {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat}
  * for more details.
  * @returns {Function} A model-to-view attribute converter.
  */
 export function modelToViewStyleAttribute( styles ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const eventType = evt.name.split( ':' )[ 0 ];
-		const consumableType = eventType + ':imageStyle';
-
-		if ( !consumable.test( data.item, consumableType ) ) {
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
 			return;
 		}
 
 		// Check if there is class name associated with given value.
 		const newStyle = getStyleByName( data.attributeNewValue, styles );
 		const oldStyle = getStyleByName( data.attributeOldValue, styles );
+
 		const viewElement = conversionApi.mapper.toViewElement( data.item );
+		const viewWriter = conversionApi.writer;
 
-		const isRemovalHandled = handleRemoval( eventType, oldStyle, viewElement );
-		const isAdditionHandled = handleAddition( eventType, newStyle, viewElement );
+		if ( oldStyle ) {
+			viewWriter.removeClass( oldStyle.className, viewElement );
+		}
 
-		// https://github.com/ckeditor/ckeditor5-image/issues/132
-		if ( isRemovalHandled || isAdditionHandled ) {
-			consumable.consume( data.item, consumableType );
+		if ( newStyle ) {
+			viewWriter.addClass( newStyle.className, viewElement );
 		}
 	};
 }
@@ -43,59 +42,42 @@ export function modelToViewStyleAttribute( styles ) {
 /**
  * Returns a view-to-model converter converting image CSS classes to a proper value in the model.
  *
- * @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>} styles Styles for which the converter is created.
+ * @param {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} styles Styles for which the converter is created.
  * @returns {Function} A view-to-model converter.
  */
 export function viewToModelStyleAttribute( styles ) {
 	// Convert only non–default styles.
 	const filteredStyles = styles.filter( style => !style.isDefault );
 
-	return ( evt, data, consumable, conversionApi ) => {
-		for ( const style of filteredStyles ) {
-			viewToModelImageStyle( style, data, consumable, conversionApi );
+	return ( evt, data, conversionApi ) => {
+		if ( !data.modelRange ) {
+			return;
 		}
-	};
-}
 
-// Converter from view to model converting single style.
-// For more information see {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher};
-//
-// @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
-// @param {Object} data
-// @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable
-// @param {Object} conversionApi
-function viewToModelImageStyle( style, data, consumable, conversionApi ) {
-	const viewFigureElement = data.input;
-	const modelImageElement = data.output;
-
-	// *** Step 1: Validate conversion.
-	// Check if view element has proper class to consume.
-	if ( !consumable.test( viewFigureElement, { class: style.className } ) ) {
-		return;
-	}
-
-	// Check if figure is converted to image.
-	if ( !isImage( modelImageElement ) ) {
-		return;
-	}
-
-	// Check if image element can be placed in current context wit additional attribute.
-	const attributes = [ ...modelImageElement.getAttributeKeys(), 'imageStyle' ];
+		const viewFigureElement = data.viewItem;
+		const modelImageElement = first( data.modelRange.getItems() );
 
-	if ( !conversionApi.schema.check( { name: 'image', inside: data.context, attributes } ) ) {
-		return;
-	}
+		// Check if `imageStyle` attribute is allowed for current element.
+		if ( !conversionApi.schema.checkAttribute( modelImageElement, 'imageStyle' ) ) {
+			return;
+		}
 
-	// *** Step2: Convert to model.
-	consumable.consume( viewFigureElement, { class: style.className } );
-	modelImageElement.setAttribute( 'imageStyle', style.name );
+		// Convert style one by one.
+		for ( const style of filteredStyles ) {
+			// Try to consume class corresponding with style.
+			if ( conversionApi.consumable.consume( viewFigureElement, { class: style.className } ) ) {
+				// And convert this style to model attribute.
+				conversionApi.writer.setAttribute( 'imageStyle', style.name, modelImageElement );
+			}
+		}
+	};
 }
 
 // Returns style with given `name` from array of styles.
 //
 // @param {String} name
-// @param {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat> } styles
-// @return {module:image/imagestyle/imagestyleengine~ImageStyleFormat|undefined}
+// @param {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat> } styles
+// @return {module:image/imagestyle/imagestyleediting~ImageStyleFormat|undefined}
 function getStyleByName( name, styles ) {
 	for ( const style of styles ) {
 		if ( style.name === name ) {
@@ -103,37 +85,3 @@ function getStyleByName( name, styles ) {
 		}
 	}
 }
-
-// Handles converting removal of the attribute.
-// Returns `true` when handling was processed correctly and further conversion can be performed.
-//
-// @param {String} eventType Type of the event.
-// @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
-// @param {module:engine/view/element~Element} viewElement
-// @returns {Boolean} Whether the change was handled.
-function handleRemoval( eventType, style, viewElement ) {
-	if ( style && ( eventType == 'changeAttribute' || eventType == 'removeAttribute' ) ) {
-		viewElement.removeClass( style.className );
-
-		return true;
-	}
-
-	return false;
-}
-
-// Handles converting addition of the attribute.
-// Returns `true` when handling was processed correctly and further conversion can be performed.
-//
-// @param {String} eventType Type of the event.
-// @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} style
-// @param {module:engine/view/element~Element} viewElement
-// @returns {Boolean} Whether the change was handled.
-function handleAddition( evenType, style, viewElement ) {
-	if ( style && ( evenType == 'addAttribute' || evenType == 'changeAttribute' ) ) {
-		viewElement.addClass( style.className );
-
-		return true;
-	}
-
-	return false;
-}

+ 36 - 26
packages/ckeditor5-image/src/imagestyle/imagestylecommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -20,71 +20,81 @@ export default class ImageStyleCommand extends Command {
 	 * Creates an instance of the image style command. Each command instance is handling one style.
 	 *
 	 * @param {module:core/editor/editor~Editor} editor The editor instance.
-	 * @param {module:image/imagestyle/imagestyleengine~ImageStyleFormat} styles A style to be applied by this command.
+	 * @param {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} styles A styles that this command supports.
 	 */
-	constructor( editor, style ) {
+	constructor( editor, styles ) {
 		super( editor );
 
 		/**
-		 * The value of the command &mdash; `true` if a style handled by the command is applied on a currently selected image,
-		 * `false` otherwise.
+		 * Cached name of the default style if present. If there is no default style it defaults to `false`.
 		 *
-		 * @readonly
-		 * @observable
-		 * @member {Boolean} #value
+		 * @type {Boolean|String}
+		 * @private
 		 */
+		this._defaultStyle = false;
 
 		/**
 		 * A style handled by this command.
 		 *
 		 * @readonly
-		 * @member {module:image/imagestyle/imagestyleengine~ImageStyleFormat} #style
+		 * @member {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} #styles
 		 */
-		this.style = style;
+		this.styles = styles.reduce( ( styles, style ) => {
+			styles[ style.name ] = style;
+
+			if ( style.isDefault ) {
+				this._defaultStyle = style.name;
+			}
+
+			return styles;
+		}, {} );
 	}
 
 	/**
 	 * @inheritDoc
 	 */
 	refresh() {
-		const element = this.editor.document.selection.getSelectedElement();
+		const element = this.editor.model.document.selection.getSelectedElement();
 
 		this.isEnabled = isImage( element );
 
 		if ( !element ) {
 			this.value = false;
-		} else if ( this.style.isDefault ) {
-			this.value = !element.hasAttribute( 'imageStyle' );
+		} else if ( element.hasAttribute( 'imageStyle' ) ) {
+			const attributeValue = element.getAttribute( 'imageStyle' );
+			this.value = this.styles[ attributeValue ] ? attributeValue : false;
 		} else {
-			this.value = ( element.getAttribute( 'imageStyle' ) == this.style.name );
+			this.value = this._defaultStyle;
 		}
 	}
 
 	/**
 	 * Executes the command.
 	 *
-	 * @fires execute
+	 *		editor.execute( 'imageStyle', { value: 'side' } );
+	 *
 	 * @param {Object} options
-	 * @param {module:engine/model/batch~Batch} [options.batch] A batch to collect all the change steps. A new batch will be
-	 * created if this option is not set.
+	 * @param {String} options.value The name of the style (based on the
+	 * {@link module:image/image~ImageConfig#styles `image.styles`} configuration option).
+	 * @fires execute
 	 */
 	execute( options = {} ) {
-		if ( this.value ) {
+		const styleName = options.value;
+
+		if ( !this.styles[ styleName ] ) {
 			return;
 		}
 
-		const doc = this.editor.document;
-		const imageElement = doc.selection.getSelectedElement();
-
-		doc.enqueueChanges( () => {
-			const batch = options.batch || doc.batch();
+		const model = this.editor.model;
+		const imageElement = model.document.selection.getSelectedElement();
 
+		model.change( writer => {
 			// Default style means that there is no `imageStyle` attribute in the model.
 			// https://github.com/ckeditor/ckeditor5-image/issues/147
-			if ( this.style.isDefault ) {
-				batch.removeAttribute( imageElement, 'imageStyle' );
+			if ( this.styles[ styleName ].isDefault ) {
+				writer.removeAttribute( 'imageStyle', imageElement );
 			} else {
-				batch.setAttribute( imageElement, 'imageStyle', this.style.name );
+				writer.setAttribute( 'imageStyle', styleName, imageElement );
 			}
 		} );
 	}

+ 102 - 0
packages/ckeditor5-image/src/imagestyle/imagestyleediting.js

@@ -0,0 +1,102 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagestyle/imagestyleediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ImageStyleCommand from './imagestylecommand';
+import ImageEditing from '../image/imageediting';
+import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters';
+import { normalizeImageStyles } from './utils';
+
+/**
+ * The image style engine plugin. It sets the default configuration, creates converters and registers
+ * {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}.
+ *
+ * @extends {module:core/plugin~Plugin}
+ */
+export default class ImageStyleEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ImageEditing ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ImageStyleEditing';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const schema = editor.model.schema;
+		const data = editor.data;
+		const editing = editor.editing;
+
+		// Define default configuration.
+		editor.config.define( 'image.styles', [ 'full', 'side' ] );
+
+		// Get configuration.
+		const styles = normalizeImageStyles( editor.config.get( 'image.styles' ) );
+
+		// Allow imageStyle attribute in image.
+		// We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559.
+		schema.extend( 'image', { allowAttributes: 'imageStyle' } );
+
+		// Converters for imageStyle attribute from model to view.
+		const modelToViewConverter = modelToViewStyleAttribute( styles );
+		editing.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter );
+		data.downcastDispatcher.on( 'attribute:imageStyle:image', modelToViewConverter );
+
+		// Converter for figure element from view to model.
+		data.upcastDispatcher.on( 'element:figure', viewToModelStyleAttribute( styles ), { priority: 'low' } );
+
+		// Register imageStyle command.
+		editor.commands.add( 'imageStyle', new ImageStyleCommand( editor, styles ) );
+	}
+}
+
+/**
+ * Image style format descriptor.
+ *
+ *		import fullSizeIcon from 'path/to/icon.svg';
+ *
+ *		const imageStyleFormat = {
+ *			name: 'fullSize',
+ *			icon: fullSizeIcon,
+ *			title: 'Full size image',
+ *			className: 'image-full-size'
+ *		}
+ *
+ * @typedef {Object} module:image/imagestyle/imagestyleediting~ImageStyleFormat
+ *
+ * @property {String} name The unique name of the style. It will be used to:
+ *
+ * * store the chosen style in the model by setting the `imageStyle` attribute of the `<image>` element,
+ * * as a value of the {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand#execute `imageStyle` command},
+ * * when registering button for each of the styles (`'imageStyle:{name}'`) in the
+ * {@link module:ui/componentfactory~ComponentFactory UI components factory} (this functionality is provided by the
+ * {@link module:image/imagestyle/imagestyleui~ImageStyleUI} plugin),
+ *
+ * @property {Boolean} [isDefault] When set, the style will be used as the default one.
+ * A default style does not apply any CSS class to the view element.
+ *
+ * @property {String} icon One of the following to be used when creating the style's button:
+ *
+ * * An SVG icon source (as an XML string),
+ * * One of {@link module:image/imagestyle/utils~defaultIcons} to use a default icon provided by the plugin.
+ *
+ * @property {String} title The style's title.
+ *
+ * @property {String} className The CSS class used to represent the style in view.
+ */

+ 0 - 298
packages/ckeditor5-image/src/imagestyle/imagestyleengine.js

@@ -1,298 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module image/imagestyle/imagestyleengine
- */
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ImageStyleCommand from './imagestylecommand';
-import ImageEngine from '../image/imageengine';
-import { viewToModelStyleAttribute, modelToViewStyleAttribute } from './converters';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
-import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
-import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
-import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
-
-/**
- * The image style engine plugin. It sets the default configuration, creates converters and registers
- * {@link module:image/imagestyle/imagestylecommand~ImageStyleCommand ImageStyleCommand}.
- *
- * @extends {module:core/plugin~Plugin}
- */
-export default class ImageStyleEngine extends Plugin {
-	/**
-	 * @inheritDoc
-	 */
-	static get requires() {
-		return [ ImageEngine ];
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get pluginName() {
-		return 'ImageStyleEngine';
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const doc = editor.document;
-		const schema = doc.schema;
-		const data = editor.data;
-		const editing = editor.editing;
-
-		// Define default configuration.
-		editor.config.define( 'image.styles', [ 'imageStyleFull', 'imageStyleSide' ] );
-
-		// Get configuration.
-		const styles = this.imageStyles;
-
-		// Allow imageStyle attribute in image.
-		// We could call it 'style' but https://github.com/ckeditor/ckeditor5-engine/issues/559.
-		schema.allow( { name: 'image', attributes: 'imageStyle', inside: '$root' } );
-
-		// Converters for imageStyle attribute from model to view.
-		const modelToViewConverter = modelToViewStyleAttribute( styles );
-		editing.modelToView.on( 'addAttribute:imageStyle:image', modelToViewConverter );
-		data.modelToView.on( 'addAttribute:imageStyle:image', modelToViewConverter );
-		editing.modelToView.on( 'changeAttribute:imageStyle:image', modelToViewConverter );
-		data.modelToView.on( 'changeAttribute:imageStyle:image', modelToViewConverter );
-		editing.modelToView.on( 'removeAttribute:imageStyle:image', modelToViewConverter );
-		data.modelToView.on( 'removeAttribute:imageStyle:image', modelToViewConverter );
-
-		// Converter for figure element from view to model.
-		data.viewToModel.on( 'element:figure', viewToModelStyleAttribute( styles ), { priority: 'low' } );
-
-		// Register separate command for each style.
-		for ( const style of styles ) {
-			editor.commands.add( style.name, new ImageStyleCommand( editor, style ) );
-		}
-	}
-
-	/**
-	 * Returns {@link module:image/image~ImageConfig#styles} array with items normalized in the
-	 * {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat} format, translated
-	 * `title` and a complete `icon` markup for each style.
-	 *
-	 * @readonly
-	 * @type {Array.<module:image/imagestyle/imagestyleengine~ImageStyleFormat>}
-	 */
-	get imageStyles() {
-		// Return cached value if there is one to improve the performance.
-		if ( this._cachedImageStyles ) {
-			return this._cachedImageStyles;
-		}
-
-		const styles = [];
-		const editor = this.editor;
-		const titles = this.localizedDefaultStylesTitles;
-		const configuredStyles = editor.config.get( 'image.styles' );
-
-		for ( let style of configuredStyles ) {
-			style = normalizeStyle( style );
-
-			// Localize the titles of the styles, if a title corresponds with
-			// a localized default provided by the plugin.
-			if ( titles[ style.title ] ) {
-				style.title = titles[ style.title ];
-			}
-
-			// Don't override the user-defined styles array, clone it instead.
-			styles.push( style );
-		}
-
-		return ( this._cachedImageStyles = styles );
-	}
-
-	/**
-	 * Returns the default localized style titles provided by the plugin e.g. ready to
-	 * use in the {@link #imageStyles}.
-	 *
-	 * The following localized titles corresponding with
-	 * {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultStyles} are available:
-	 *
-	 * * `'Full size image'`,
-	 * * `'Side image'`,
-	 * * `'Left aligned image'`,
-	 * * `'Centered image'`,
-	 * * `'Right aligned image'`
-	 *
-	 * @readonly
-	 * @type {Object.<String,String>}
-	 */
-	get localizedDefaultStylesTitles() {
-		const t = this.editor.t;
-
-		return {
-			'Full size image': t( 'Full size image' ),
-			'Side image': t( 'Side image' ),
-			'Left aligned image': t( 'Left aligned image' ),
-			'Centered image': t( 'Centered image' ),
-			'Right aligned image': t( 'Right aligned image' ),
-		};
-	}
-}
-
-/**
- * Default image styles provided by the plugin, which can be referred in the
- * {@link module:image/image~ImageConfig#styles} config.
- *
- * Among them, 2 default semantic content styles are available:
- *
- * * `imageStyleFull` is a full–width image without any CSS class,
- * * `imageStyleSide` is a side image styled with the `image-style-side` CSS class
- *
- * There are also 3 styles focused on formatting:
- *
- * * `imageStyleAlignLeft` aligns the image to the left using the `image-style-align-left` class,
- * * `imageStyleAlignCenter` centers the image to the left using the `image-style-align-center` class,
- * * `imageStyleAlignRight` aligns the image to the right using the `image-style-align-right` class,
- *
- * @member {Object.<String,Object>}
- */
-ImageStyleEngine.defaultStyles = {
-	// This option is equal to situation when no style is applied.
-	imageStyleFull: {
-		name: 'imageStyleFull',
-		title: 'Full size image',
-		icon: fullWidthIcon,
-		isDefault: true
-	},
-
-	// This represents side image.
-	imageStyleSide: {
-		name: 'imageStyleSide',
-		title: 'Side image',
-		icon: rightIcon,
-		className: 'image-style-side'
-	},
-
-	// This style represents an imaged aligned to the left.
-	imageStyleAlignLeft: {
-		name: 'imageStyleAlignLeft',
-		title: 'Left aligned image',
-		icon: leftIcon,
-		className: 'image-style-align-left'
-	},
-
-	// This style represents a centered imaged.
-	imageStyleAlignCenter: {
-		name: 'imageStyleAlignCenter',
-		title: 'Centered image',
-		icon: centerIcon,
-		className: 'image-style-align-center'
-	},
-
-	// This style represents an imaged aligned to the right.
-	imageStyleAlignRight: {
-		name: 'imageStyleAlignRight',
-		title: 'Right aligned image',
-		icon: rightIcon,
-		className: 'image-style-align-right'
-	}
-};
-
-/**
- * Default image style icons provided by the plugin, which can be referred in the
- * {@link module:image/image~ImageConfig#styles} config.
- *
- * There are 4 icons available: `'full'`, `'left'`, `'center'` and `'right'`.
- *
- * @member {Object.<String, String>}
- */
-ImageStyleEngine.defaultIcons = {
-	full: fullWidthIcon,
-	left: leftIcon,
-	right: rightIcon,
-	center: centerIcon,
-};
-
-// Normalizes an image style provided in the {@link module:image/image~ImageConfig#styles}
-// and returns it in a {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}.
-//
-// @private
-// @param {Object} style
-// @returns {@link module:image/imagestyle/imagestyleengine~ImageStyleFormat}
-function normalizeStyle( style ) {
-	const defaultStyles = ImageStyleEngine.defaultStyles;
-	const defaultIcons = ImageStyleEngine.defaultIcons;
-
-	// Just the name of the style has been passed.
-	if ( typeof style == 'string' ) {
-		// If it's one of the defaults, just use it.
-		// Clone the style to avoid overriding defaults.
-		if ( defaultStyles[ style ] ) {
-			style = Object.assign( {}, defaultStyles[ style ] );
-		}
-		// If it's just a name but none of the defaults, warn because probably it's a mistake.
-		else {
-			log.warn(
-				'image-style-not-found: There is no such image style of given name.',
-				{ name: style }
-			);
-
-			// Normalize the style anyway to prevent errors.
-			style = {
-				name: style
-			};
-		}
-	}
-
-	// If an object style has been passed and if the name matches one of the defaults,
-	// extend it with defaults – the user wants to customize a default style.
-	// Note: Don't override the user–defined style object, clone it instead.
-	else if ( defaultStyles[ style.name ] ) {
-		const defaultStyle = defaultStyles[ style.name ];
-		const extendedStyle = Object.assign( {}, style );
-
-		for ( const prop in defaultStyle ) {
-			if ( !style.hasOwnProperty( prop ) ) {
-				extendedStyle[ prop ] = defaultStyle[ prop ];
-			}
-		}
-
-		style = extendedStyle;
-	}
-
-	// If an icon is defined as a string and correspond with a name
-	// in default icons, use the default icon provided by the plugin.
-	if ( typeof style.icon == 'string' && defaultIcons[ style.icon ] ) {
-		style.icon = defaultIcons[ style.icon ];
-	}
-
-	return style;
-}
-
-/**
- * Image style format descriptor.
- *
- *		import fullWidthIcon from 'path/to/icon.svg`;
- *
- *		const imageStyleFormat = {
- *			name: 'fullSizeImage',
- *			icon: fullWidthIcon,
- *			title: 'Full size image',
- *			className: 'image-full-size'
- *		}
- *
- * @typedef {Object} module:image/imagestyle/imagestyleengine~ImageStyleFormat
- * @property {String} name The unique name of the style. It will be used to:
- * * register the {@link module:core/command~Command command} which will apply this style,
- * * store the style's button in the editor {@link module:ui/componentfactory~ComponentFactory},
- * * store the style in the `imageStyle` model attribute.
- * @property {Boolean} [isDefault] When set, the style will be used as the default one.
- * A default style does not apply any CSS class to the view element.
- * @property {String} icon One of the following to be used when creating the style's button:
- *  * An SVG icon source (as an XML string),
- *  * One of {@link module:image/imagestyle/imagestyleengine~ImageStyleEngine.defaultIcons} to use a default icon provided by the plugin.
- * @property {String} title The style's title.
- * @property {String} className The CSS class used to represent the style in view.
- */

+ 111 - 0
packages/ckeditor5-image/src/imagestyle/imagestyleui.js

@@ -0,0 +1,111 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagestyle/imagestyleui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import { normalizeImageStyles } from './utils';
+
+import '../../theme/imagestyle.css';
+
+/**
+ * The image style UI plugin.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageStyleUI extends Plugin {
+	/**
+	 * Returns the default localized style titles provided by the plugin.
+	 *
+	 * The following localized titles corresponding with
+	 * {@link module:image/imagestyle/utils~defaultStyles} are available:
+	 *
+	 * * `'Full size image'`,
+	 * * `'Side image'`,
+	 * * `'Left aligned image'`,
+	 * * `'Centered image'`,
+	 * * `'Right aligned image'`
+	 *
+	 * @returns {Object.<String,String>}
+	 */
+	get localizedDefaultStylesTitles() {
+		const t = this.editor.t;
+
+		return {
+			'Full size image': t( 'Full size image' ),
+			'Side image': t( 'Side image' ),
+			'Left aligned image': t( 'Left aligned image' ),
+			'Centered image': t( 'Centered image' ),
+			'Right aligned image': t( 'Right aligned image' )
+		};
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const configuredStyles = editor.config.get( 'image.styles' );
+
+		const translatedStyles = translateStyles( normalizeImageStyles( configuredStyles ), this.localizedDefaultStylesTitles );
+
+		for ( const style of translatedStyles ) {
+			this._createButton( style );
+		}
+	}
+
+	/**
+	 * Creates a button for each style and stores it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
+	 *
+	 * @private
+	 * @param {module:image/imagestyle/imagestyleediting~ImageStyleFormat} style
+	 */
+	_createButton( style ) {
+		const editor = this.editor;
+
+		const componentName = `imageStyle:${ style.name }`;
+
+		editor.ui.componentFactory.add( componentName, locale => {
+			const command = editor.commands.get( 'imageStyle' );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: style.title,
+				icon: style.icon,
+				tooltip: true
+			} );
+
+			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
+			view.bind( 'isOn' ).to( command, 'value', value => value === style.name );
+
+			this.listenTo( view, 'execute', () => editor.execute( 'imageStyle', { value: style.name } ) );
+
+			return view;
+		} );
+	}
+}
+
+/**
+ * Returns translated `title` from the passed styles array.
+ *
+ * @param {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>} styles
+ * @param titles
+ * @returns {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>}
+ */
+function translateStyles( styles, titles ) {
+	for ( const style of styles ) {
+		// Localize the titles of the styles, if a title corresponds with
+		// a localized default provided by the plugin.
+		if ( titles[ style.title ] ) {
+			style.title = titles[ style.title ];
+		}
+	}
+
+	return styles;
+}

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

@@ -0,0 +1,154 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagestyle/utils
+ */
+
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
+import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
+import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
+import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
+
+/**
+ * Default image styles provided by the plugin, which can be referred in the
+ * {@link module:image/image~ImageConfig#styles} config.
+ *
+ * Among them, 2 default semantic content styles are available:
+ *
+ * * `full` is a full–width image without any CSS class,
+ * * `side` is a side image styled with the `image-style-side` CSS class
+ *
+ * There are also 3 styles focused on formatting:
+ *
+ * * `alignLeft` aligns the image to the left using the `image-style-align-left` class,
+ * * `alignCenter` centers the image to the left using the `image-style-align-center` class,
+ * * `alignRight` aligns the image to the right using the `image-style-align-right` class,
+ *
+ * @member {Object.<String,Object>}
+ */
+const defaultStyles = {
+	// This option is equal to situation when no style is applied.
+	full: {
+		name: 'full',
+		title: 'Full size image',
+		icon: fullWidthIcon,
+		isDefault: true
+	},
+
+	// This represents side image.
+	side: {
+		name: 'side',
+		title: 'Side image',
+		icon: rightIcon,
+		className: 'image-style-side'
+	},
+
+	// This style represents an imaged aligned to the left.
+	alignLeft: {
+		name: 'alignLeft',
+		title: 'Left aligned image',
+		icon: leftIcon,
+		className: 'image-style-align-left'
+	},
+
+	// This style represents a centered imaged.
+	alignCenter: {
+		name: 'alignCenter',
+		title: 'Centered image',
+		icon: centerIcon,
+		className: 'image-style-align-center'
+	},
+
+	// This style represents an imaged aligned to the right.
+	alignRight: {
+		name: 'alignRight',
+		title: 'Right aligned image',
+		icon: rightIcon,
+		className: 'image-style-align-right'
+	}
+};
+
+/**
+ * Default image style icons provided by the plugin, which can be referred in the
+ * {@link module:image/image~ImageConfig#styles} config.
+ *
+ * There are 4 icons available: `'full'`, `'left'`, `'center'` and `'right'`.
+ *
+ * @member {Object.<String, String>}
+ */
+const defaultIcons = {
+	full: fullWidthIcon,
+	left: leftIcon,
+	right: rightIcon,
+	center: centerIcon
+};
+
+/**
+ * Returns {@link module:image/image~ImageConfig#styles} array with items normalized in the
+ * {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat} format and a complete `icon` markup for each style.
+ *
+ * @returns {Array.<module:image/imagestyle/imagestyleediting~ImageStyleFormat>}
+ */
+export function normalizeImageStyles( configuredStyles = [] ) {
+	return configuredStyles
+		.map( _normalizeStyle )
+		.map( style => Object.assign( {}, style ) );
+}
+
+// Normalizes an image style provided in the {@link module:image/image~ImageConfig#styles}
+// and returns it in a {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat}.
+//
+// @param {Object} style
+// @returns {@link module:image/imagestyle/imagestyleediting~ImageStyleFormat}
+function _normalizeStyle( style ) {
+	// Just the name of the style has been passed.
+	if ( typeof style == 'string' ) {
+		const styleName = style;
+
+		// If it's one of the defaults, just use it.
+		if ( defaultStyles[ styleName ] ) {
+			// Clone the style to avoid overriding defaults.
+			style = Object.assign( {}, defaultStyles[ styleName ] );
+		}
+		// If it's just a name but none of the defaults, warn because probably it's a mistake.
+		else {
+			log.warn(
+				'image-style-not-found: There is no such image style of given name.',
+				{ name: styleName }
+			);
+
+			// Normalize the style anyway to prevent errors.
+			style = {
+				name: styleName
+			};
+		}
+	}
+	// If an object style has been passed and if the name matches one of the defaults,
+	// extend it with defaults – the user wants to customize a default style.
+	// Note: Don't override the user–defined style object, clone it instead.
+	else if ( defaultStyles[ style.name ] ) {
+		const defaultStyle = defaultStyles[ style.name ];
+		const extendedStyle = Object.assign( {}, style );
+
+		for ( const prop in defaultStyle ) {
+			if ( !style.hasOwnProperty( prop ) ) {
+				extendedStyle[ prop ] = defaultStyle[ prop ];
+			}
+		}
+
+		style = extendedStyle;
+	}
+
+	// If an icon is defined as a string and correspond with a name
+	// in default icons, use the default icon provided by the plugin.
+	if ( typeof style.icon == 'string' && defaultIcons[ style.icon ] ) {
+		style.icon = defaultIcons[ style.icon ];
+	}
+
+	return style;
+}

+ 6 - 173
packages/ckeditor5-image/src/imagetextalternative.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,21 +8,14 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import ImageTextAlternativeEngine from './imagetextalternative/imagetextalternativeengine';
-import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
-import TextAlternativeFormView from './imagetextalternative/ui/textalternativeformview';
-import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
-import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
-import { repositionContextualBalloon, getBalloonPositionData } from './image/ui/utils';
-import { isImageWidgetSelected } from './image/utils';
-
-import '../theme/imagetextalternative/theme.scss';
+import ImageTextAlternativeEditing from './imagetextalternative/imagetextalternativeediting';
+import ImageTextAlternativeUI from './imagetextalternative/imagetextalternativeui';
 
 /**
  * The image text alternative plugin.
  *
- * The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
+ * It loads the {@link module:image/imagetextalternative/imagetextalternativeediting~ImageTextAlternativeEditing}
+ * and {@link module:image/imagetextalternative/imagetextalternativeui~ImageTextAlternativeUI} plugins.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -31,7 +24,7 @@ export default class ImageTextAlternative extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ ImageTextAlternativeEngine, ContextualBalloon ];
+		return [ ImageTextAlternativeEditing, ImageTextAlternativeUI ];
 	}
 
 	/**
@@ -40,164 +33,4 @@ export default class ImageTextAlternative extends Plugin {
 	static get pluginName() {
 		return 'ImageTextAlternative';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		this._createButton();
-		this._createForm();
-	}
-
-	/**
-	 * Creates a button showing the balloon panel for changing the image text alternative and
-	 * registers it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
-	 *
-	 * @private
-	 */
-	_createButton() {
-		const editor = this.editor;
-		const command = editor.commands.get( 'imageTextAlternative' );
-		const t = editor.t;
-
-		editor.ui.componentFactory.add( 'imageTextAlternative', locale => {
-			const view = new ButtonView( locale );
-
-			view.set( {
-				label: t( 'Change image text alternative' ),
-				icon: textAlternativeIcon,
-				tooltip: true
-			} );
-
-			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
-
-			this.listenTo( view, 'execute', () => this._showForm() );
-
-			return view;
-		} );
-	}
-
-	/**
-	 * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
-	 * form.
-	 *
-	 * @private
-	 */
-	_createForm() {
-		const editor = this.editor;
-		const editingView = editor.editing.view;
-
-		/**
-		 * The contextual balloon plugin instance.
-		 *
-		 * @private
-		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
-		 */
-		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
-
-		/**
-		 * A form containing a textarea and buttons, used to change the `alt` text value.
-		 *
-		 * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView} #form
-		 */
-		this._form = new TextAlternativeFormView( editor.locale );
-
-		// Render the form so its #element is available for clickOutsideHandler.
-		this._form.render();
-
-		this.listenTo( this._form, 'submit', () => {
-			editor.execute( 'imageTextAlternative', {
-				newValue: this._form.labeledInput.inputView.element.value
-			} );
-
-			this._hideForm( true );
-		} );
-
-		this.listenTo( this._form, 'cancel', () => {
-			this._hideForm( true );
-		} );
-
-		// Close the form on Esc key press.
-		this._form.keystrokes.set( 'Esc', ( data, cancel ) => {
-			this._hideForm( true );
-			cancel();
-		} );
-
-		// Reposition the balloon or hide the form if an image widget is no longer selected.
-		this.listenTo( editingView, 'render', () => {
-			if ( !isImageWidgetSelected( editingView.selection ) ) {
-				this._hideForm( true );
-			} else if ( this._isVisible ) {
-				repositionContextualBalloon( editor );
-			}
-		}, { priority: 'low' } );
-
-		// Close on click outside of balloon panel element.
-		clickOutsideHandler( {
-			emitter: this._form,
-			activator: () => this._isVisible,
-			contextElements: [ this._form.element ],
-			callback: () => this._hideForm()
-		} );
-	}
-
-	/**
-	 * Shows the {@link #_form} in the {@link #_balloon}.
-	 *
-	 * @private
-	 */
-	_showForm() {
-		if ( this._isVisible ) {
-			return;
-		}
-
-		const editor = this.editor;
-		const command = editor.commands.get( 'imageTextAlternative' );
-		const labeledInput = this._form.labeledInput;
-
-		if ( !this._balloon.hasView( this._form ) ) {
-			this._balloon.add( {
-				view: this._form,
-				position: getBalloonPositionData( editor )
-			} );
-		}
-
-		// Make sure that each time the panel shows up, the field remains in sync with the value of
-		// the command. If the user typed in the input, then canceled the balloon (`labeledInput#value`
-		// stays unaltered) and re-opened it without changing the value of the command, they would see the
-		// old value instead of the actual value of the command.
-		// https://github.com/ckeditor/ckeditor5-image/issues/114
-		labeledInput.value = labeledInput.inputView.element.value = command.value || '';
-
-		this._form.labeledInput.select();
-	}
-
-	/**
-	 * Removes the {@link #_form} from the {@link #_balloon}.
-	 *
-	 * @param {Boolean} [focusEditable=false] Controls whether the editing view is focused afterwards.
-	 * @private
-	 */
-	_hideForm( focusEditable ) {
-		if ( !this._isVisible ) {
-			return;
-		}
-
-		this._balloon.remove( this._form );
-
-		if ( focusEditable ) {
-			this.editor.editing.view.focus();
-		}
-	}
-
-	/**
-	 * Returns `true` when the {@link #_form} is the visible view
-	 * in the {@link #_balloon}.
-	 *
-	 * @private
-	 * @type {Boolean}
-	 */
-	get _isVisible() {
-		return this._balloon.visibleView == this._form;
-	}
 }

+ 6 - 10
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativecommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -28,7 +28,7 @@ export default class ImageTextAlternativeCommand extends Command {
 	 * @inheritDoc
 	 */
 	refresh() {
-		const element = this.editor.document.selection.getSelectedElement();
+		const element = this.editor.model.document.selection.getSelectedElement();
 
 		this.isEnabled = isImage( element );
 
@@ -45,17 +45,13 @@ export default class ImageTextAlternativeCommand extends Command {
 	 * @fires execute
 	 * @param {Object} options
 	 * @param {String} options.newValue The new value of the `alt` attribute to set.
-	 * @param {module:engine/model/batch~Batch} [options.batch] A batch to collect all the change steps. A new batch will be
-	 * created if this option is not set.
 	 */
 	execute( options ) {
-		const doc = this.editor.document;
-		const imageElement = doc.selection.getSelectedElement();
+		const model = this.editor.model;
+		const imageElement = model.document.selection.getSelectedElement();
 
-		doc.enqueueChanges( () => {
-			const batch = options.batch || doc.batch();
-
-			batch.setAttribute( imageElement, 'alt', options.newValue );
+		model.change( writer => {
+			writer.setAttribute( 'alt', options.newValue, imageElement );
 		} );
 	}
 }

+ 3 - 3
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeengine.js → packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeediting.js

@@ -1,10 +1,10 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /**
- * @module image/imagetextalternative/imagetextalternativeengine
+ * @module image/imagetextalternative/imagetextalternativeediting
  */
 
 import ImageTextAlternativeCommand from './imagetextalternativecommand';
@@ -16,7 +16,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  *
  * @extends module:core/plugin~Plugin
  */
-export default class ImageTextAlternativeEngine extends Plugin {
+export default class ImageTextAlternativeEditing extends Plugin {
 	/**
 	 * @inheritDoc
 	 */

+ 194 - 0
packages/ckeditor5-image/src/imagetextalternative/imagetextalternativeui.js

@@ -0,0 +1,194 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imagetextalternative/imagetextalternativeui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
+import TextAlternativeFormView from './ui/textalternativeformview';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
+import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg';
+import { repositionContextualBalloon, getBalloonPositionData } from '../image/ui/utils';
+import { isImageWidgetSelected } from '../image/utils';
+
+/**
+ * The image text alternative UI plugin.
+ *
+ * The plugin uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageTextAlternativeUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ContextualBalloon ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this._createButton();
+		this._createForm();
+	}
+
+	/**
+	 * Creates a button showing the balloon panel for changing the image text alternative and
+	 * registers it in the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
+	 *
+	 * @private
+	 */
+	_createButton() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		editor.ui.componentFactory.add( 'imageTextAlternative', locale => {
+			const command = editor.commands.get( 'imageTextAlternative' );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: t( 'Change image text alternative' ),
+				icon: textAlternativeIcon,
+				tooltip: true
+			} );
+
+			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
+
+			this.listenTo( view, 'execute', () => this._showForm() );
+
+			return view;
+		} );
+	}
+
+	/**
+	 * Creates the {@link module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
+	 * form.
+	 *
+	 * @private
+	 */
+	_createForm() {
+		const editor = this.editor;
+		const view = editor.editing.view;
+		const viewDocument = view.document;
+
+		/**
+		 * The contextual balloon plugin instance.
+		 *
+		 * @private
+		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
+		 */
+		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
+
+		/**
+		 * A form containing a textarea and buttons, used to change the `alt` text value.
+		 *
+		 * @member {module:image/imagetextalternative/ui/textalternativeformview~TextAlternativeFormView}
+		 */
+		this._form = new TextAlternativeFormView( editor.locale );
+
+		// Render the form so its #element is available for clickOutsideHandler.
+		this._form.render();
+
+		this.listenTo( this._form, 'submit', () => {
+			editor.execute( 'imageTextAlternative', {
+				newValue: this._form.labeledInput.inputView.element.value
+			} );
+
+			this._hideForm( true );
+		} );
+
+		this.listenTo( this._form, 'cancel', () => {
+			this._hideForm( true );
+		} );
+
+		// Close the form on Esc key press.
+		this._form.keystrokes.set( 'Esc', ( data, cancel ) => {
+			this._hideForm( true );
+			cancel();
+		} );
+
+		// Reposition the balloon or hide the form if an image widget is no longer selected.
+		this.listenTo( view, 'render', () => {
+			if ( !isImageWidgetSelected( viewDocument.selection ) ) {
+				this._hideForm( true );
+			} else if ( this._isVisible ) {
+				repositionContextualBalloon( editor );
+			}
+		} );
+
+		// Close on click outside of balloon panel element.
+		clickOutsideHandler( {
+			emitter: this._form,
+			activator: () => this._isVisible,
+			contextElements: [ this._form.element ],
+			callback: () => this._hideForm()
+		} );
+	}
+
+	/**
+	 * Shows the {@link #_form} in the {@link #_balloon}.
+	 *
+	 * @private
+	 */
+	_showForm() {
+		if ( this._isVisible ) {
+			return;
+		}
+
+		const editor = this.editor;
+		const command = editor.commands.get( 'imageTextAlternative' );
+		const labeledInput = this._form.labeledInput;
+
+		if ( !this._balloon.hasView( this._form ) ) {
+			this._balloon.add( {
+				view: this._form,
+				position: getBalloonPositionData( editor )
+			} );
+		}
+
+		// Make sure that each time the panel shows up, the field remains in sync with the value of
+		// the command. If the user typed in the input, then canceled the balloon (`labeledInput#value`
+		// stays unaltered) and re-opened it without changing the value of the command, they would see the
+		// old value instead of the actual value of the command.
+		// https://github.com/ckeditor/ckeditor5-image/issues/114
+		labeledInput.value = labeledInput.inputView.element.value = command.value || '';
+
+		this._form.labeledInput.select();
+	}
+
+	/**
+	 * Removes the {@link #_form} from the {@link #_balloon}.
+	 *
+	 * @param {Boolean} [focusEditable=false] Controls whether the editing view is focused afterwards.
+	 * @private
+	 */
+	_hideForm( focusEditable ) {
+		if ( !this._isVisible ) {
+			return;
+		}
+
+		this._balloon.remove( this._form );
+
+		if ( focusEditable ) {
+			this.editor.editing.view.focus();
+		}
+	}
+
+	/**
+	 * Returns `true` when the {@link #_form} is the visible view
+	 * in the {@link #_balloon}.
+	 *
+	 * @private
+	 * @type {Boolean}
+	 */
+	get _isVisible() {
+		return this._balloon.visibleView == this._form;
+	}
+}

+ 19 - 21
packages/ckeditor5-image/src/imagetextalternative/ui/textalternativeformview.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -19,6 +19,10 @@ import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
 
+import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
+import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
+import '../../../theme/textalternativeform.css';
+
 /**
  * The TextAlternativeFormView class.
  *
@@ -61,7 +65,7 @@ export default class TextAlternativeFormView extends View {
 		 *
 		 * @member {module:ui/button/buttonview~ButtonView} #saveButtonView
 		 */
-		this.saveButtonView = this._createButton( t( 'Save' ) );
+		this.saveButtonView = this._createButton( t( 'Save' ), checkIcon );
 		this.saveButtonView.type = 'submit';
 
 		/**
@@ -69,7 +73,7 @@ export default class TextAlternativeFormView extends View {
 		 *
 		 * @member {module:ui/button/buttonview~ButtonView} #cancelButtonView
 		 */
-		this.cancelButtonView = this._createButton( t( 'Cancel' ), 'cancel' );
+		this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'cancel' );
 
 		/**
 		 * A collection of views which can be focused in the form.
@@ -113,7 +117,7 @@ export default class TextAlternativeFormView extends View {
 
 			attributes: {
 				class: [
-					'cke-text-alternative-form',
+					'ck-text-alternative-form',
 				],
 
 				// https://github.com/ckeditor/ckeditor5-image/issues/40
@@ -122,20 +126,8 @@ export default class TextAlternativeFormView extends View {
 
 			children: [
 				this.labeledInput,
-				{
-					tag: 'div',
-
-					attributes: {
-						class: [
-							'cke-text-alternative-form__actions'
-						]
-					},
-
-					children: [
-						this.saveButtonView,
-						this.cancelButtonView
-					]
-				}
+				this.saveButtonView,
+				this.cancelButtonView
 			]
 		} );
 	}
@@ -165,14 +157,18 @@ export default class TextAlternativeFormView extends View {
 	 *
 	 * @private
 	 * @param {String} label The button label
+	 * @param {String} icon The button's icon.
 	 * @param {String} [eventName] The event name that the ButtonView#execute event will be delegated to.
 	 * @returns {module:ui/button/buttonview~ButtonView} The button view instance.
 	 */
-	_createButton( label, eventName ) {
+	_createButton( label, icon, eventName ) {
 		const button = new ButtonView( this.locale );
 
-		button.label = label;
-		button.withText = true;
+		button.set( {
+			label,
+			icon,
+			tooltip: true
+		} );
 
 		if ( eventName ) {
 			button.delegate( 'execute' ).to( this, eventName );
@@ -190,7 +186,9 @@ export default class TextAlternativeFormView extends View {
 	_createLabeledInputView() {
 		const t = this.locale.t;
 		const labeledInput = new LabeledInputView( this.locale, InputTextView );
+
 		labeledInput.label = t( 'Text alternative' );
+		labeledInput.inputView.placeholder = t( 'Text alternative' );
 
 		return labeledInput;
 	}

+ 25 - 13
packages/ckeditor5-image/src/imagetoolbar.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -13,7 +13,7 @@ import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextu
 import { isImageWidgetSelected } from './image/utils';
 import { repositionContextualBalloon, getBalloonPositionData } from './image/ui/utils';
 
-const balloonClassName = 'ck-toolbar-container ck-editor-toolbar-container';
+const balloonClassName = 'ck-toolbar-container';
 
 /**
  * The image toolbar class. Creates an image toolbar that shows up when the image widget is selected.
@@ -40,6 +40,25 @@ export default class ImageToolbar extends Plugin {
 		return 'ImageToolbar';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
+
+		// If `BalloonToolbar` plugin is loaded, it should be disabled for images
+		// which have their own toolbar to avoid duplication.
+		// https://github.com/ckeditor/ckeditor5-image/issues/110
+		if ( balloonToolbar ) {
+			this.listenTo( balloonToolbar, 'show', evt => {
+				if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) {
+					evt.stop();
+				}
+			}, { priority: 'high' } );
+		}
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -69,20 +88,13 @@ export default class ImageToolbar extends Plugin {
 		 */
 		this._toolbar = new ToolbarView();
 
-		// Add CSS class to the toolbar.
-		this._toolbar.extendTemplate( {
-			attributes: {
-				class: 'ck-editor-toolbar'
-			}
-		} );
-
 		// Add buttons to the toolbar.
 		this._toolbar.fillFromConfig( toolbarConfig, editor.ui.componentFactory );
 
 		// Show balloon panel each time image widget is selected.
 		this.listenTo( editor.editing.view, 'render', () => {
 			this._checkIsVisible();
-		}, { priority: 'low' } );
+		} );
 
 		// There is no render method after focus is back in editor, we need to check if balloon panel should be visible.
 		this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
@@ -102,7 +114,7 @@ export default class ImageToolbar extends Plugin {
 		if ( !editor.ui.focusTracker.isFocused ) {
 			this._hideToolbar();
 		} else {
-			if ( isImageWidgetSelected( editor.editing.view.selection ) ) {
+			if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) {
 				this._showToolbar();
 			} else {
 				this._hideToolbar();
@@ -166,10 +178,10 @@ export default class ImageToolbar extends Plugin {
  * * {@link module:image/imagetextalternative~ImageTextAlternative}.
  *
  * Three toolbar items will be available in {@link module:ui/componentfactory~ComponentFactory}:
- * `'imageStyleFull'`, `'imageStyleSide'`, and `'imageTextAlternative'` so you can configure the toolbar like this:
+ * `'imageStyle:full'`, `'imageStyle:side'`, and `'imageTextAlternative'` so you can configure the toolbar like this:
  *
  *		const imageConfig = {
- *			toolbar: [ 'imageStyleFull', 'imageStyleSide', '|', 'imageTextAlternative' ]
+ *			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
  *		};
  *
  * Of course, the same buttons can also be used in the

+ 39 - 0
packages/ckeditor5-image/src/imageupload.js

@@ -0,0 +1,39 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imageupload
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ImageUploadUI from './imageupload/imageuploadui';
+import ImageUploadProgress from './imageupload/imageuploadprogress';
+import ImageUploadEditing from './imageupload/imageuploadediting';
+
+/**
+ * Image upload plugin.
+ *
+ * This plugin do not do anything directly, but loads set of specific plugins to enable image uploading:
+ * * {@link module:image/imageupload/imageuploadediting~ImageUploadEditing},
+ * * {@link module:image/imageupload/imageuploadui~ImageUploadUI},
+ * * {@link module:image/imageupload/imageuploadprogress~ImageUploadProgress}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageUpload extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'ImageUpload';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ImageUploadEditing, ImageUploadUI, ImageUploadProgress ];
+	}
+}

+ 67 - 0
packages/ckeditor5-image/src/imageupload/imageuploadcommand.js

@@ -0,0 +1,67 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
+import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
+import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
+import Command from '@ckeditor/ckeditor5-core/src/command';
+
+/**
+ * @module image/imageupload/imageuploadcommand
+ */
+
+/**
+ * Image upload command.
+ *
+ * @extends module:core/command~Command
+ */
+export default class ImageUploadCommand extends Command {
+	/**
+	 * Executes the command.
+	 *
+	 * @fires execute
+	 * @param {Object} options Options for executed command.
+	 * @param {File} options.file Image file to upload.
+	 * @param {module:engine/model/position~Position} [options.insertAt] Position at which the image should be inserted.
+	 * If the position is not specified the image will be inserted into the current selection.
+	 * Note: You can use the {@link module:upload/utils~findOptimalInsertionPosition} function to calculate
+	 * (e.g. based on the current selection) a position which is more optimal from UX perspective.
+	 */
+	execute( options ) {
+		const editor = this.editor;
+		const doc = editor.model.document;
+		const file = options.file;
+		const fileRepository = editor.plugins.get( FileRepository );
+
+		editor.model.change( writer => {
+			const loader = fileRepository.createLoader( file );
+
+			// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
+			if ( !loader ) {
+				return;
+			}
+
+			const imageElement = new ModelElement( 'image', {
+				uploadId: loader.id
+			} );
+
+			let insertAtSelection;
+
+			if ( options.insertAt ) {
+				insertAtSelection = new ModelSelection( [ new ModelRange( options.insertAt ) ] );
+			} else {
+				insertAtSelection = doc.selection;
+			}
+
+			editor.model.insertContent( imageElement, insertAtSelection );
+
+			// Inserting an image might've failed due to schema regulations.
+			if ( imageElement.parent ) {
+				writer.setSelection( ModelRange.createOn( imageElement ) );
+			}
+		} );
+	}
+}

+ 215 - 0
packages/ckeditor5-image/src/imageupload/imageuploadediting.js

@@ -0,0 +1,215 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imageupload/imageuploadediting
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
+import ImageUploadCommand from '../../src/imageupload/imageuploadcommand';
+import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
+import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
+import { isImageType, findOptimalInsertionPosition } from '../../src/imageupload/utils';
+
+/**
+ * Image upload editing plugin.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageUploadEditing extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ FileRepository, Notification ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const doc = editor.model.document;
+		const schema = editor.model.schema;
+		const fileRepository = editor.plugins.get( FileRepository );
+
+		// Setup schema to allow uploadId and uploadStatus for images.
+		schema.extend( 'image', {
+			allowAttributes: [ 'uploadId', 'uploadStatus' ]
+		} );
+
+		// Register imageUpload command.
+		editor.commands.add( 'imageUpload', new ImageUploadCommand( editor ) );
+
+		// Execute imageUpload command when image is dropped or pasted.
+		editor.editing.view.document.on( 'clipboardInput', ( evt, data ) => {
+			// Skip if non empty HTML data is included.
+			// https://github.com/ckeditor/ckeditor5-upload/issues/68
+			if ( isHtmlIncluded( data.dataTransfer ) ) {
+				return;
+			}
+
+			let targetModelSelection = new ModelSelection(
+				data.targetRanges.map( viewRange => editor.editing.mapper.toModelRange( viewRange ) )
+			);
+
+			for ( const file of data.dataTransfer.files ) {
+				const insertAt = findOptimalInsertionPosition( targetModelSelection );
+
+				if ( isImageType( file ) ) {
+					editor.execute( 'imageUpload', { file, insertAt } );
+					evt.stop();
+				}
+
+				// Use target ranges only for the first image. Then, use that image position
+				// so we keep adding the next ones after the previous one.
+				targetModelSelection = doc.selection;
+			}
+		} );
+
+		// Prevents from browser redirecting to the dropped image.
+		editor.editing.view.document.on( 'dragover', ( evt, data ) => {
+			data.preventDefault();
+		} );
+
+		doc.on( 'change', () => {
+			const changes = doc.differ.getChanges( { includeChangesInGraveyard: true } );
+
+			for ( const entry of changes ) {
+				if ( entry.type == 'insert' && entry.name == 'image' ) {
+					const item = entry.position.nodeAfter;
+					const isInGraveyard = entry.position.root.rootName == '$graveyard';
+
+					// Check if the image element still has upload id.
+					const uploadId = item.getAttribute( 'uploadId' );
+
+					if ( !uploadId ) {
+						continue;
+					}
+
+					// Check if the image is loaded on this client.
+					const loader = fileRepository.loaders.get( uploadId );
+
+					if ( !loader ) {
+						continue;
+					}
+
+					if ( isInGraveyard ) {
+						// If the image was inserted to the graveyard - abort the loading process.
+						loader.abort();
+					} else if ( loader.status == 'idle' ) {
+						// If the image was inserted into content and has not been loaded, start loading it.
+						this._load( loader, item );
+					}
+				}
+			}
+		} );
+	}
+
+	/**
+	 * Performs image loading. Image is read from the disk and temporary data is displayed, after uploading process
+	 * is complete we replace temporary data with target image from the server.
+	 *
+	 * @private
+	 * @param {module:upload/filerepository~FileLoader} loader
+	 * @param {module:engine/model/element~Element} imageElement
+	 */
+	_load( loader, imageElement ) {
+		const editor = this.editor;
+		const model = editor.model;
+		const t = editor.locale.t;
+		const fileRepository = editor.plugins.get( FileRepository );
+		const notification = editor.plugins.get( Notification );
+
+		model.enqueueChange( 'transparent', writer => {
+			writer.setAttribute( 'uploadStatus', 'reading', imageElement );
+		} );
+
+		loader.read()
+			.then( data => {
+				const viewFigure = editor.editing.mapper.toViewElement( imageElement );
+				const viewImg = viewFigure.getChild( 0 );
+				const promise = loader.upload();
+
+				editor.editing.view.change( writer => {
+					writer.setAttribute( 'src', data, viewImg );
+				} );
+
+				model.enqueueChange( 'transparent', writer => {
+					writer.setAttribute( 'uploadStatus', 'uploading', imageElement );
+				} );
+
+				return promise;
+			} )
+			.then( data => {
+				model.enqueueChange( 'transparent', writer => {
+					writer.setAttributes( { uploadStatus: 'complete', src: data.default }, imageElement );
+
+					// Srcset attribute for responsive images support.
+					let maxWidth = 0;
+					const srcsetAttribute = Object.keys( data )
+						// Filter out keys that are not integers.
+						.filter( key => {
+							const width = parseInt( key, 10 );
+
+							if ( !isNaN( width ) ) {
+								maxWidth = Math.max( maxWidth, width );
+
+								return true;
+							}
+						} )
+
+						// Convert each key to srcset entry.
+						.map( key => `${ data[ key ] } ${ key }w` )
+
+						// Join all entries.
+						.join( ', ' );
+
+					if ( srcsetAttribute != '' ) {
+						writer.setAttribute( 'srcset', {
+							data: srcsetAttribute,
+							width: maxWidth
+						}, imageElement );
+					}
+				} );
+
+				clean();
+			} )
+			.catch( msg => {
+				// Might be 'aborted'.
+				if ( loader.status == 'error' ) {
+					notification.showWarning( msg, {
+						title: t( 'Upload failed' ),
+						namespace: 'upload'
+					} );
+				}
+
+				clean();
+
+				// Permanently remove image from insertion batch.
+				model.enqueueChange( 'transparent', writer => {
+					writer.remove( imageElement );
+				} );
+			} );
+
+		function clean() {
+			model.enqueueChange( 'transparent', writer => {
+				writer.removeAttribute( 'uploadId', imageElement );
+				writer.removeAttribute( 'uploadStatus', imageElement );
+			} );
+
+			fileRepository.destroyLoader( loader );
+		}
+	}
+}
+
+// Returns true if non-empty `text/html` is included in data transfer.
+//
+// @param {module:clipboard/datatransfer~DataTransfer} dataTransfer
+// @returns {Boolean}
+export function isHtmlIncluded( dataTransfer ) {
+	return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
+}

+ 143 - 0
packages/ckeditor5-image/src/imageupload/imageuploadprogress.js

@@ -0,0 +1,143 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imageupload/imageuploadprogress
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
+import uploadingPlaceholder from '../../theme/icons/image_placeholder.svg';
+import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
+import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
+
+import '../../theme/imageuploadprogress.css';
+
+/**
+ * Image upload progress plugin.
+ * Shows placeholder when image is read from disk and progress bar while image is uploading.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageUploadProgress extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * Image's placeholder that is displayed before real image data can be accessed.
+		 *
+		 * @protected
+		 * @member {String} #placeholder
+		 */
+		this.placeholder = 'data:image/svg+xml;utf8,' + encodeURIComponent( uploadingPlaceholder );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		// Upload status change - update image's view according to that status.
+		editor.editing.downcastDispatcher.on( 'attribute:uploadStatus:image', ( ...args ) => this.uploadStatusChange( ...args ) );
+	}
+
+	/**
+	 * This ethod is called each time image's `uploadStatus` attribute is changed.
+	 *
+	 * @param {module:utils/eventinfo~EventInfo} evt Object containing information about the fired event.
+	 * @param {Object} data Additional information about the change.
+	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
+	 * @param {Object} conversionApi
+	 */
+	uploadStatusChange( evt, data, conversionApi ) {
+		const editor = this.editor;
+		const modelImage = data.item;
+		const uploadId = modelImage.getAttribute( 'uploadId' );
+
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) || !uploadId ) {
+			return;
+		}
+
+		const fileRepository = editor.plugins.get( FileRepository );
+		const placeholder = this.placeholder;
+		const status = data.attributeNewValue;
+		const viewFigure = editor.editing.mapper.toViewElement( modelImage );
+		const viewWriter = conversionApi.writer;
+
+		// Show placeholder with infinite progress bar on the top while image is read from disk.
+		if ( status == 'reading' ) {
+			viewWriter.addClass( [ 'ck-appear', 'ck-infinite-progress', 'ck-image-upload-placeholder' ], viewFigure );
+			const viewImg = viewFigure.getChild( 0 );
+			viewWriter.setAttribute( 'src', placeholder, viewImg );
+
+			return;
+		}
+
+		// Show progress bar on the top of the image when image is uploading.
+		if ( status == 'uploading' ) {
+			const loader = fileRepository.loaders.get( uploadId );
+
+			if ( loader ) {
+				const progressBar = createProgressBar( viewWriter );
+
+				viewWriter.removeClass( [ 'ck-infinite-progress', 'ck-image-upload-placeholder' ], viewFigure );
+				viewWriter.insert( ViewPosition.createAt( viewFigure, 'end' ), progressBar );
+
+				// Update progress bar width when uploadedPercent is changed.
+				loader.on( 'change:uploadedPercent', ( evt, name, value ) => {
+					editor.editing.view.change( writer => {
+						writer.setStyle( 'width', value + '%', progressBar );
+					} );
+				} );
+			}
+
+			return;
+		}
+
+		// Hide progress bar and clean up classes.
+		const progressBar = getProgressBar( viewFigure );
+
+		if ( progressBar ) {
+			viewWriter.remove( ViewRange.createOn( progressBar ) );
+		} else {
+			viewWriter.removeClass( 'ck-infinite-progress', viewFigure );
+		}
+
+		viewWriter.removeClass( [ 'ck-appear', 'ck-image-upload-placeholder' ], viewFigure );
+	}
+}
+
+// Symbol added to progress bar UIElement to distinguish it from other elements.
+const progressBarSymbol = Symbol( 'progress-bar' );
+
+// Create progress bar element using {@link module:engine/view/uielement~UIElement}.
+//
+// @private
+// @param {module:engine/view/writer~Writer} writer
+// @returns {module:engine/view/uielement~UIElement}
+function createProgressBar( writer ) {
+	const progressBar = writer.createUIElement( 'div', { class: 'ck-progress-bar' } );
+	writer.setCustomProperty( progressBarSymbol, true, progressBar );
+
+	return progressBar;
+}
+
+// Returns progress bar {@link module:engine/view/uielement~UIElement} from image figure element. Returns `undefined` if
+// progress bar element is not found.
+//
+// @private
+// @param {module:engine/view/element~Element} imageFigure
+// @returns {module:engine/view/uielement~UIElement|undefined}
+function getProgressBar( imageFigure ) {
+	for ( const child of imageFigure.getChildren() ) {
+		if ( child.getCustomProperty( progressBarSymbol ) ) {
+			return child;
+		}
+	}
+}

+ 60 - 0
packages/ckeditor5-image/src/imageupload/imageuploadui.js

@@ -0,0 +1,60 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/imageupload/imageuploadui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview';
+import imageIcon from '@ckeditor/ckeditor5-core/theme/icons/image.svg';
+import { isImageType, findOptimalInsertionPosition } from './utils';
+
+/**
+ * Image upload button plugin.
+ * Adds `imageUpload` button to UI component factory.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class ImageUploadUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		// Setup `imageUpload` button.
+		editor.ui.componentFactory.add( 'imageUpload', locale => {
+			const view = new FileDialogButtonView( locale );
+			const command = editor.commands.get( 'imageUpload' );
+
+			view.set( {
+				acceptedType: 'image/*',
+				allowMultipleFiles: true
+			} );
+
+			view.buttonView.set( {
+				label: t( 'Insert image' ),
+				icon: imageIcon,
+				tooltip: true
+			} );
+
+			view.buttonView.bind( 'isEnabled' ).to( command );
+
+			view.on( 'done', ( evt, files ) => {
+				for ( const file of Array.from( files ) ) {
+					const insertAt = findOptimalInsertionPosition( editor.model.document.selection );
+
+					if ( isImageType( file ) ) {
+						editor.execute( 'imageUpload', { file, insertAt } );
+					}
+				}
+			} );
+
+			return view;
+		} );
+	}
+}

+ 67 - 0
packages/ckeditor5-image/src/imageupload/utils.js

@@ -0,0 +1,67 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module upload/utils
+ */
+
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
+
+/**
+ * Checks if given file is an image.
+ *
+ * @param {File} file
+ * @returns {Boolean}
+ */
+export function isImageType( file ) {
+	const types = /^image\/(jpeg|png|gif|bmp)$/;
+
+	return types.test( file.type );
+}
+
+/**
+ * Returns a model position which is optimal (in terms of UX) for inserting an image.
+ *
+ * For instance, if a selection is in a middle of a paragraph, position before this paragraph
+ * will be returned, so that it's not split. If the selection is at the end of a paragraph,
+ * position after this paragraph will be returned.
+ *
+ * Note: If selection is placed in an empty block, that block will be returned. If that position
+ * is then passed to {@link module:engine/model/model~Model#insertContent}
+ * that block will be fully replaced by the image.
+ *
+ * @param {module:engine/model/selection~Selection} selection Selection based on which the
+ * insertion position should be calculated.
+ * @returns {module:engine/model/position~Position} The optimal position.
+ */
+export function findOptimalInsertionPosition( selection ) {
+	const selectedElement = selection.getSelectedElement();
+
+	if ( selectedElement ) {
+		return ModelPosition.createAfter( selectedElement );
+	}
+
+	const firstBlock = selection.getSelectedBlocks().next().value;
+
+	if ( firstBlock ) {
+		// If inserting into an empty block – return position in that block. It will get
+		// replaced with the image by insertContent(). #42.
+		if ( firstBlock.isEmpty ) {
+			return ModelPosition.createAt( firstBlock );
+		}
+
+		const positionAfter = ModelPosition.createAfter( firstBlock );
+
+		// If selection is at the end of the block - return position after the block.
+		if ( selection.focus.isTouching( positionAfter ) ) {
+			return positionAfter;
+		}
+
+		// Otherwise return position before the block.
+		return ModelPosition.createBefore( firstBlock );
+	}
+
+	return selection.focus;
+}

+ 24 - 22
packages/ckeditor5-image/tests/image.js

@@ -1,11 +1,11 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Image from '../src/image';
-import ImageEngine from '../src/image/imageengine';
+import ImageEditing from '../src/image/imageediting';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 import ImageTextAlternative from '../src/imagetextalternative';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -14,7 +14,7 @@ import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 describe( 'Image', () => {
-	let editorElement, editor, document, viewDocument;
+	let editorElement, model, view, editor, document, viewDocument;
 
 	beforeEach( () => {
 		editorElement = global.document.createElement( 'div' );
@@ -26,8 +26,10 @@ describe( 'Image', () => {
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				document = editor.document;
-				viewDocument = editor.editing.view;
+				model = editor.model;
+				document = model.document;
+				view = editor.editing.view;
+				viewDocument = editor.editing.view.document;
 			} );
 	} );
 
@@ -41,8 +43,8 @@ describe( 'Image', () => {
 		expect( editor.plugins.get( Image ) ).to.instanceOf( Image );
 	} );
 
-	it( 'should load ImageEngine plugin', () => {
-		expect( editor.plugins.get( ImageEngine ) ).to.instanceOf( ImageEngine );
+	it( 'should load ImageEditing plugin', () => {
+		expect( editor.plugins.get( ImageEditing ) ).to.instanceOf( ImageEditing );
 	} );
 
 	it( 'should load Widget plugin', () => {
@@ -55,10 +57,10 @@ describe( 'Image', () => {
 
 	describe( 'selection', () => {
 		it( 'should create fake selection', () => {
-			setModelData( document, '[<image alt="alt text" src="foo.png"></image>]' );
+			setModelData( model, '[<image alt="alt text" src="foo.png"></image>]' );
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'[<figure class="ck-widget ck-widget_selected image" contenteditable="false">' +
 					'<img alt="alt text" src="foo.png"></img>' +
 				'</figure>]'
 			);
@@ -68,10 +70,10 @@ describe( 'Image', () => {
 		} );
 
 		it( 'should create proper fake selection label when alt attribute is empty', () => {
-			setModelData( document, '[<image src="foo.png" alt=""></image>]' );
+			setModelData( model, '[<image src="foo.png" alt=""></image>]' );
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'[<figure class="ck-widget ck-widget_selected image" contenteditable="false">' +
 				'<img alt="" src="foo.png"></img>' +
 				'</figure>]'
 			);
@@ -81,30 +83,30 @@ describe( 'Image', () => {
 		} );
 
 		it( 'should remove selected class from previously selected element', () => {
-			setModelData( document,
+			setModelData( model,
 				'[<image src="foo.png" alt="alt text"></image>]' +
 				'<image src="foo.png" alt="alt text"></image>'
 			);
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'[<figure class="ck-widget ck-widget_selected image" contenteditable="false">' +
 				'<img alt="alt text" src="foo.png"></img>' +
 				'</figure>]' +
-				'<figure class="image ck-widget" contenteditable="false">' +
+				'<figure class="ck-widget image" contenteditable="false">' +
 				'<img alt="alt text" src="foo.png"></img>' +
 				'</figure>'
 			);
 
-			document.enqueueChanges( () => {
+			model.change( writer => {
 				const secondImage = document.getRoot().getChild( 1 );
-				document.selection.setRanges( [ ModelRange.createOn( secondImage ) ] );
+				writer.setSelection( ModelRange.createOn( secondImage ) );
 			} );
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false">' +
 				'<img alt="alt text" src="foo.png"></img>' +
 				'</figure>' +
-				'[<figure class="image ck-widget ck-widget_selected" contenteditable="false">' +
+				'[<figure class="ck-widget ck-widget_selected image" contenteditable="false">' +
 				'<img alt="alt text" src="foo.png"></img>' +
 				'</figure>]'
 			);

+ 145 - 217
packages/ckeditor5-image/tests/image/converters.js

@@ -1,73 +1,87 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import {
 	viewFigureToModel,
-	createImageAttributeConverter,
-	convertHoistableImage,
-	hoistImageThroughElement
+	modelToViewAttributeConverter
 } from '../../src/image/converters';
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import { createImageViewElement } from '../../src/image/imageengine';
 import { toImageWidget } from '../../src/image/utils';
-import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
-import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
-import ViewEmptyElement from '@ckeditor/ckeditor5-engine/src/view/emptyelement';
-import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
-import { convertToModelFragment } from '@ckeditor/ckeditor5-engine/src/conversion/view-to-model-converters';
+import { createImageViewElement } from '../../src/image/imageediting';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 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;
+	let editor, model, document, viewDocument;
 
 	beforeEach( () => {
 		return VirtualTestEditor.create()
 			.then( newEditor => {
 				editor = newEditor;
-				document = editor.document;
+				model = editor.model;
+				document = model.document;
 				viewDocument = editor.editing.view;
-				const schema = document.schema;
 
-				schema.registerItem( 'image' );
-				schema.requireAttributes( 'image', [ 'src' ] );
-				schema.allow( { name: 'image', attributes: [ 'alt', 'src' ], inside: '$root' } );
-				schema.objects.add( 'image' );
+				const schema = model.schema;
+
+				schema.register( 'image', {
+					allowWhere: '$block',
+					allowAttributes: [ 'alt', 'src' ],
+					isObject: true,
+					isBlock: true
+				} );
+
+				const editingElementCreator = ( modelElement, viewWriter ) =>
+					toImageWidget( createImageViewElement( viewWriter ), viewWriter, '' );
 
-				buildModelConverter().for( editor.editing.modelToView )
-					.fromElement( 'image' )
-					.toElement( () => toImageWidget( createImageViewElement() ) );
+				editor.conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
+					model: 'image',
+					view: editingElementCreator
+				} ) );
 
-				createImageAttributeConverter( [ editor.editing.modelToView ], 'src' );
-				createImageAttributeConverter( [ editor.editing.modelToView ], 'alt' );
+				editor.conversion.for( 'downcast' )
+					.add( modelToViewAttributeConverter( 'src' ) )
+					.add( modelToViewAttributeConverter( 'alt' ) );
 			} );
 	} );
 
 	describe( 'viewFigureToModel', () => {
-		function expectModel( model ) {
-			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( model );
+		function expectModel( data ) {
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( data );
 		}
 
-		let dispatcher, schema, imgConverterCalled;
+		let schema, imgConverterCalled;
 
 		beforeEach( () => {
-			imgConverterCalled = false;
-
-			schema = document.schema;
-			schema.allow( { name: '$text', inside: 'image' } );
-
-			dispatcher = editor.data.viewToModel;
-			dispatcher.on( 'element:figure', viewFigureToModel() );
-			dispatcher.on( 'element:img', ( evt, data, consumable ) => {
-				if ( consumable.consume( data.input, { name: true, attribute: 'src' } ) ) {
-					data.output = new ModelElement( 'image', { src: data.input.getAttribute( 'src' ) } );
-
-					imgConverterCalled = true;
-				}
-			} );
+			// Since this part of test tests only view->model conversion editing pipeline is not necessary
+			// so defining model->view converters won't be necessary.
+			editor.editing.destroy();
+
+			schema = model.schema;
+			schema.extend( '$text', { allowIn: 'image' } );
+
+			editor.conversion.for( 'upcast' )
+				.add( viewFigureToModel() )
+				.add( upcastElementToElement( {
+					view: {
+						name: 'img',
+						attribute: {
+							src: true
+						}
+					},
+					model: ( viewImage, writer ) => {
+						imgConverterCalled = true;
+
+						return writer.createElement( 'image', { src: viewImage.getAttribute( 'src' ) } );
+					}
+				} ) );
 		} );
 
 		it( 'should find img element among children and convert it using already defined converters', () => {
@@ -77,14 +91,13 @@ describe( 'Image converters', () => {
 			expect( imgConverterCalled ).to.be.true;
 		} );
 
-		it( 'should convert non-img children in image context and append them to model image element', () => {
-			buildViewConverter().for( editor.data.viewToModel ).fromElement( 'foo' ).toElement( 'foo' );
-			buildViewConverter().for( editor.data.viewToModel ).fromElement( 'bar' ).toElement( 'bar' );
-
-			schema.registerItem( 'foo' );
-			schema.registerItem( 'bar' );
+		it( 'should convert children allowed by schema and omit disallowed', () => {
+			editor.conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'foo', model: 'foo' } ) );
+			editor.conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'bar', model: 'bar' } ) );
 
-			schema.allow( { name: 'foo', inside: 'image' } );
+			schema.register( 'foo', { allowIn: 'image' } );
+			// Is allowed in root, but should not try to split image element.
+			schema.register( 'bar', { allowIn: '$root' } );
 
 			editor.setData( '<figure class="image">x<img src="foo.png" />y<foo></foo><bar></bar></figure>' );
 
@@ -92,12 +105,74 @@ describe( 'Image converters', () => {
 			expectModel( '<image src="foo.png">xy<foo></foo></image>' );
 		} );
 
-		it( 'should be possible to overwrite', () => {
-			dispatcher.on( 'element:figure', ( evt, data, consumable ) => {
-				consumable.consume( data.input, { name: true } );
-				consumable.consume( data.input.getChild( 0 ), { name: true } );
+		it( 'should split parent element when image is not allowed - in the middle', () => {
+			editor.conversion.for( 'upcast' ).add( upcastElementToElement( { view: 'div', model: 'div' } ) );
+
+			schema.register( 'div', { inheritAllFrom: '$block' } );
+			schema.extend( 'image', { disallowIn: 'div' } );
+
+			editor.setData(
+				'<div>' +
+					'abc' +
+					'<figure class="image">' +
+						'<img src="foo.jpg"/>' +
+					'</figure>' +
+					'def' +
+				'</div>'
+			);
+
+			expectModel( '<div>abc</div><image src="foo.jpg"></image><div>def</div>' );
+		} );
 
-				data.output = new ModelElement( 'myImage', { data: { src: data.input.getChild( 0 ).getAttribute( 'src' ) } } );
+		it( 'should split parent element when image is not allowed - at the end', () => {
+			editor.conversion.elementToElement( { model: 'div', view: 'div' } );
+
+			schema.register( 'div', { inheritAllFrom: '$block' } );
+			schema.extend( 'image', { disallowIn: 'div' } );
+
+			editor.setData(
+				'<div>' +
+					'abc' +
+					'<figure class="image">' +
+						'<img src="foo.jpg"/>' +
+					'</figure>' +
+				'</div>'
+			);
+
+			expectModel( '<div>abc</div><image src="foo.jpg"></image>' );
+		} );
+
+		it( 'should split parent element when image is not allowed - at the beginning', () => {
+			editor.conversion.elementToElement( { model: 'div', view: 'div' } );
+
+			schema.register( 'div', { inheritAllFrom: '$block' } );
+			schema.extend( 'image', { disallowIn: 'div' } );
+
+			editor.setData(
+				'<div>' +
+					'<figure class="image">' +
+						'<img src="foo.jpg"/>' +
+					'</figure>' +
+					'def' +
+				'</div>'
+			);
+
+			expectModel( '<image src="foo.jpg"></image><div>def</div>' );
+		} );
+
+		it( 'should be possible to overwrite', () => {
+			editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.viewItem, { name: true } );
+				conversionApi.consumable.consume( data.viewItem.getChild( 0 ), { name: true } );
+
+				const element = conversionApi.writer.createElement( 'myImage', {
+					data: {
+						src: data.viewItem.getChild( 0 ).getAttribute( 'src' )
+					}
+				} );
+				conversionApi.writer.insert( element, data.modelCursor );
+				data.modelRange = ModelRange.createOn( element );
+				data.modelCursor = data.modelRange.end;
 			}, { priority: 'high' } );
 
 			editor.setData( '<figure class="image"><img src="foo.png" />xyz</figure>' );
@@ -132,201 +207,54 @@ describe( 'Image converters', () => {
 
 	describe( 'modelToViewAttributeConverter', () => {
 		it( 'should convert adding attribute to image', () => {
-			setModelData( document, '<image src=""></image>' );
+			setModelData( model, '<image src=""></image>' );
 			const image = document.getRoot().getChild( 0 );
 
-			document.enqueueChanges( () => {
-				const batch = document.batch();
-
-				batch.setAttribute( image, 'alt', 'foo bar' );
+			model.change( writer => {
+				writer.setAttribute( 'alt', 'foo bar', image );
 			} );
 
 			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img alt="foo bar" src=""></img></figure>'
+				'<figure class="ck-widget image" contenteditable="false"><img alt="foo bar" src=""></img></figure>'
 			);
 		} );
 
 		it( 'should convert removing attribute from image', () => {
-			setModelData( document, '<image src="" alt="foo bar"></image>' );
+			setModelData( model, '<image src="" alt="foo bar"></image>' );
 			const image = document.getRoot().getChild( 0 );
 
-			document.enqueueChanges( () => {
-				const batch = document.batch();
-
-				batch.removeAttribute( image, 'alt' );
+			model.change( writer => {
+				writer.removeAttribute( 'alt', image );
 			} );
 
 			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src=""></img></figure>'
+				'<figure class="ck-widget image" contenteditable="false"><img src=""></img></figure>'
 			);
 		} );
 
 		it( 'should convert change of attribute image', () => {
-			setModelData( document, '<image src="" alt="foo bar"></image>' );
+			setModelData( model, '<image src="" alt="foo bar"></image>' );
 			const image = document.getRoot().getChild( 0 );
 
-			document.enqueueChanges( () => {
-				const batch = document.batch();
-
-				batch.setAttribute( image, 'alt', 'baz quix' );
+			model.change( writer => {
+				writer.setAttribute( 'alt', 'baz quix', image );
 			} );
 
 			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img alt="baz quix" src=""></img></figure>'
+				'<figure class="ck-widget image" contenteditable="false"><img alt="baz quix" src=""></img></figure>'
 			);
 		} );
 
-		it( 'should not change attribute if change is already consumed', () => {
-			editor.editing.modelToView.on( 'changeAttribute:alt:image', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'changeAttribute:alt' );
+		it( 'should not set attribute if change was already consumed', () => {
+			editor.editing.downcastDispatcher.on( 'attribute:alt:image', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'attribute:alt' );
 			}, { priority: 'high' } );
 
-			setModelData( document, '<image src="" alt="foo bar"></image>' );
-			const image = document.getRoot().getChild( 0 );
-
-			document.enqueueChanges( () => {
-				const batch = document.batch();
-
-				batch.setAttribute( image, 'alt', 'baz quix' );
-			} );
+			setModelData( model, '<image src="" alt="foo bar"></image>' );
 
 			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img alt="foo bar" src=""></img></figure>'
+				'<figure class="ck-widget image" contenteditable="false"><img src=""></img></figure>'
 			);
 		} );
 	} );
-
-	// More integration tests are in imageengine.js.
-	describe( 'autohoist converters', () => {
-		let dispatcher, schema, imgConverterCalled, viewImg, modelDiv, modelParagraph, modelLimit;
-
-		beforeEach( () => {
-			imgConverterCalled = false;
-
-			schema = document.schema;
-
-			dispatcher = editor.data.viewToModel;
-			dispatcher.on( 'element:img', ( evt, data, consumable ) => {
-				if ( !schema.check( { name: 'image', attributes: [ 'src' ], inside: data.context } ) ) {
-					return;
-				}
-
-				if ( consumable.consume( data.input, { name: true, attribute: 'src' } ) ) {
-					data.output = new ModelElement( 'image', { src: data.input.getAttribute( 'src' ) } );
-
-					imgConverterCalled = true;
-				}
-			} );
-
-			// Make sure to pass document element to the converter and fire this callback after "normal" image callback.
-			dispatcher.on( 'element:img', convertHoistableImage, { priority: 'low' } );
-
-			viewImg = new ViewEmptyElement( 'img', { src: 'foo.jpg' } );
-
-			modelDiv = new ModelElement( 'div' );
-			modelParagraph = new ModelElement( 'paragraph' );
-			modelLimit = new ModelElement( 'limit' );
-		} );
-
-		describe( 'convertHoistableImage', () => {
-			it( 'should convert img element using already added converters if it is allowed in any point of given context #1', () => {
-				const result = dispatcher.convert( viewImg, { context: [ '$root', 'div', 'paragraph' ] } );
-
-				// `result` is a model document fragment.
-				expect( result.childCount ).to.equal( 1 );
-				expect( result.getChild( 0 ).is( 'image' ) ).to.be.true;
-				expect( result.getChild( 0 ).getAttribute( 'src' ) ).to.equal( 'foo.jpg' );
-				expect( imgConverterCalled ).to.be.true;
-			} );
-
-			it( 'should convert img element using already added converters if it is allowed in any point of given context #2', () => {
-				const result = dispatcher.convert( viewImg, { context: [ '$root', modelDiv, modelParagraph ] } );
-
-				// `result` is a model document fragment.
-				expect( result.childCount ).to.equal( 1 );
-				expect( result.getChild( 0 ).is( 'image' ) ).to.be.true;
-				expect( result.getChild( 0 ).getAttribute( 'src' ) ).to.equal( 'foo.jpg' );
-				expect( imgConverterCalled ).to.be.true;
-			} );
-
-			it( 'should not convert img element if there is no allowed context #1', () => {
-				const result = dispatcher.convert( viewImg, { context: [ 'div', 'paragraph' ] } );
-
-				// `result` is an empty model document fragment.
-				expect( result.childCount ).to.equal( 0 );
-				expect( imgConverterCalled ).to.be.false;
-			} );
-
-			it( 'should not convert img element if there is no allowed context #2', () => {
-				const result = dispatcher.convert( viewImg, { context: [ modelDiv, modelParagraph ] } );
-
-				// `result` is an empty model document fragment.
-				expect( result.childCount ).to.equal( 0 );
-				expect( imgConverterCalled ).to.be.false;
-			} );
-
-			it( 'should not convert img element if allowed context is "above" limiting element #1', () => {
-				schema.limits.add( 'limit' );
-
-				const result = dispatcher.convert( viewImg, { context: [ '$root', 'limit', 'div', 'paragraph' ] } );
-
-				// `result` is an empty model document fragment.
-				expect( result.childCount ).to.equal( 0 );
-				expect( imgConverterCalled ).to.be.false;
-			} );
-
-			it( 'should not convert img element if allowed context is "above" limiting element #2', () => {
-				schema.limits.add( 'limit' );
-
-				const result = dispatcher.convert( viewImg, { context: [ '$root', modelLimit, modelDiv, modelParagraph ] } );
-
-				// `result` is an empty model document fragment.
-				expect( result.childCount ).to.equal( 0 );
-				expect( imgConverterCalled ).to.be.false;
-			} );
-		} );
-
-		describe( 'hoistImageThroughElement', () => {
-			it( 'should hoist img element that was converted by convertHoistableImage', () => {
-				schema.registerItem( 'div', '$block' );
-
-				buildModelConverter().for( editor.data.modelToView, editor.editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
-
-				// Make sure to fire this callback after "normal" div callback.
-				dispatcher.on( 'element:div', hoistImageThroughElement, { priority: 'low' } );
-
-				// If img view element is converted it must have been converted thanks to convertHoistableImage,
-				// because image is not allowed in div.
-				const viewDiv = new ViewContainerElement( 'div', null, [ 'foo', viewImg, 'bar' ] );
-
-				const result = dispatcher.convert( viewDiv, { context: [ '$root' ] } );
-
-				// `result` is a model document fragment.
-				expect( result.childCount ).to.equal( 3 );
-				expect( result.getChild( 0 ).is( 'div' ) ).to.be.true;
-				expect( result.getChild( 1 ).is( 'image' ) ).to.be.true;
-				expect( result.getChild( 2 ).is( 'div' ) ).to.be.true;
-			} );
-
-			it( 'should work fine with elements that are converted to document fragments', () => {
-				// The example here is <p> that contains some text and an <img> and is converted in $root > div context.
-				// This way we enable autohoisting for <img> and test how it will work when <p> is converted to model document fragment.
-				schema.registerItem( 'div', '$block' );
-
-				dispatcher.on( 'element:p', convertToModelFragment() );
-				dispatcher.on( 'element:p', hoistImageThroughElement, { priority: 'low' } );
-
-				const viewDiv = new ViewContainerElement( 'p', null, [ 'foo', viewImg, 'bar' ] );
-
-				const result = dispatcher.convert( viewDiv, { context: [ '$root', 'div' ] } );
-
-				// `result` is a model document fragment.
-				expect( result.childCount ).to.equal( 3 );
-				expect( result.getChild( 0 ).is( 'text' ) ).to.be.true;
-				expect( result.getChild( 1 ).is( 'image' ) ).to.be.true;
-				expect( result.getChild( 2 ).is( 'text' ) ).to.be.true;
-			} );
-		} );
-	} );
 } );

+ 131 - 164
packages/ckeditor5-image/tests/image/imageengine.js → packages/ckeditor5-image/tests/image/imageediting.js

@@ -1,68 +1,76 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageEngine from '../../src/image/imageengine';
+import ImageEditing from '../../src/image/imageediting';
 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/image/utils';
+import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 
-describe( 'ImageEngine', () => {
-	let editor, document, viewDocument;
+describe( 'ImageEditing', () => {
+	let editor, model, document, view, viewDocument;
 
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ ImageEngine ]
+				plugins: [ ImageEditing ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				document = editor.document;
-				viewDocument = editor.editing.view;
+				model = editor.model;
+				document = model.document;
+				view = editor.editing.view;
+				viewDocument = view.document;
 			} );
 	} );
 
 	it( 'should be loaded', () => {
-		expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine );
+		expect( editor.plugins.get( ImageEditing ) ).to.be.instanceOf( ImageEditing );
 	} );
 
 	it( 'should set proper schema rules', () => {
-		expect( document.schema.check( { name: 'image', attributes: [ 'src', 'alt' ], inside: '$root' } ) ).to.be.true;
-		expect( document.schema.objects.has( 'image' ) ).to.be.true;
+		expect( model.schema.checkChild( [ '$root' ], 'image' ) ).to.be.true;
+		expect( model.schema.checkAttribute( [ '$root', 'image' ], 'src' ) ).to.be.true;
+		expect( model.schema.checkAttribute( [ '$root', 'image' ], 'alt' ) ).to.be.true;
+
+		expect( model.schema.isObject( 'image' ) ).to.be.true;
+
+		expect( model.schema.checkChild( [ '$root', 'image' ], 'image' ) ).to.be.false;
+		expect( model.schema.checkChild( [ '$root', 'image' ], '$text' ) ).to.be.false;
+		expect( model.schema.checkChild( [ '$root', '$block' ], 'image' ) ).to.be.false;
 	} );
 
 	describe( 'conversion in data pipeline', () => {
 		describe( 'model to view', () => {
 			it( 'should convert', () => {
-				setModelData( document, '<image src="foo.png" alt="alt text"></image>' );
+				setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
 
 				expect( editor.getData() ).to.equal( '<figure class="image"><img alt="alt text" src="foo.png"></figure>' );
 			} );
 
 			it( 'should convert without alt attribute', () => {
-				setModelData( document, '<image src="foo.png"></image>' );
+				setModelData( model, '<image src="foo.png"></image>' );
 
 				expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
 			} );
 
 			it( 'should convert srcset attribute to srcset and sizes attribute', () => {
-				setModelData( document,
+				setModelData( model,
 					'<image src="foo.png" alt="alt text" srcset=\'{ "data": "small.png 148w, big.png 1024w" }\'></image>'
 				);
 
-				expect( editor.getData() ).to.equal(
+				expect( normalizeHtml( editor.getData() ) ).to.equal(
 					'<figure class="image">' +
-						'<img srcset="small.png 148w, big.png 1024w" sizes="100vw" alt="alt text" src="foo.png">' +
+						'<img alt="alt text" sizes="100vw" src="foo.png" srcset="small.png 148w, big.png 1024w"></img>' +
 					'</figure>'
 				);
 			} );
 
 			it( 'should convert srcset attribute to width, srcset and add sizes attribute', () => {
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'alt="alt text" ' +
@@ -70,23 +78,21 @@ describe( 'ImageEngine', () => {
 					'</image>'
 				);
 
-				expect( editor.getData() ).to.equal(
+				expect( normalizeHtml( editor.getData() ) ).to.equal(
 					'<figure class="image">' +
-						'<img srcset="small.png 148w, big.png 1024w" sizes="100vw" width="1024" alt="alt text" src="foo.png">' +
+						'<img alt="alt text" sizes="100vw" src="foo.png" srcset="small.png 148w, big.png 1024w" width="1024"></img>' +
 					'</figure>'
 				);
 			} );
 
 			it( 'should not convert srcset attribute if is already consumed', () => {
-				editor.data.modelToView.on( 'addAttribute:srcset:image', ( evt, data, consumable ) => {
-					const parts = evt.name.split( ':' );
-					const consumableType = parts[ 0 ] + ':' + parts[ 1 ];
+				editor.data.downcastDispatcher.on( 'attribute:srcset:image', ( evt, data, conversionApi ) => {
 					const modelImage = data.item;
 
-					consumable.consume( modelImage, consumableType );
+					conversionApi.consumable.consume( modelImage, evt.name );
 				}, { priority: 'high' } );
 
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'alt="alt text" ' +
@@ -102,7 +108,7 @@ describe( 'ImageEngine', () => {
 			} );
 
 			it( 'should not convert srcset attribute if has wrong data', () => {
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'alt="alt text" ' +
@@ -110,10 +116,8 @@ describe( 'ImageEngine', () => {
 					'</image>' );
 
 				const image = document.getRoot().getChild( 0 );
-				document.enqueueChanges( () => {
-					const batch = document.batch();
-
-					batch.removeAttribute( image, 'srcset' );
+				model.change( writer => {
+					writer.removeAttribute( 'srcset', image );
 				} );
 
 				expect( editor.getData() ).to.equal(
@@ -128,88 +132,87 @@ describe( 'ImageEngine', () => {
 			it( 'should convert image figure', () => {
 				editor.setData( '<figure class="image"><img src="foo.png" alt="alt text" /></figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '<image alt="alt text" src="foo.png"></image>' );
 			} );
 
 			it( 'should not convert if there is no image class', () => {
 				editor.setData( '<figure class="quote">My quote</figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '' );
 			} );
 
 			it( 'should not convert if there is no img inside #1', () => {
 				editor.setData( '<figure class="image"></figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '' );
 			} );
 
 			it( 'should not convert if there is no img inside #2', () => {
 				editor.setData( '<figure class="image">test</figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '' );
 			} );
 
 			it( 'should convert without alt attribute', () => {
 				editor.setData( '<figure class="image"><img src="foo.png" /></figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '<image src="foo.png"></image>' );
 			} );
 
 			it( 'should not convert without src attribute', () => {
 				editor.setData( '<figure class="image"><img alt="alt text" /></figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '' );
 			} );
 
 			it( 'should not convert in wrong context', () => {
-				const data = editor.data;
-				const editing = editor.editing;
-
-				document.schema.registerItem( 'div', '$block' );
-				document.schema.disallow( { name: 'image', inside: '$root', attributes: 'src' } );
+				model.schema.register( 'div', { inheritAllFrom: '$block' } );
+				model.schema.addChildCheck( ( ctx, childDef ) => {
+					if ( ctx.endsWith( '$root' ) && childDef.name == 'image' ) {
+						return false;
+					}
+				} );
 
-				buildModelConverter().for( data.modelToView, editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
-				buildViewConverter().for( data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
+				editor.conversion.elementToElement( { model: 'div', view: 'div' } );
 
 				editor.setData( '<div><figure class="image"><img src="foo.png" alt="alt text" /></figure></div>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '<div></div>' );
 			} );
 
 			it( 'should not convert if img is already consumed', () => {
-				editor.data.viewToModel.on( 'element:figure', ( evt, data, consumable ) => {
-					const img = data.input.getChild( 0 );
-					consumable.consume( img, { name: true } );
+				editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
+					const img = data.viewItem.getChild( 0 );
+					conversionApi.consumable.consume( img, { name: true } );
 				}, { priority: 'high' } );
 
 				editor.setData( '<figure class="image"><img src="foo.png" alt="alt text" /></figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '' );
 			} );
 
 			it( 'should not convert if figure is already consumed', () => {
-				editor.data.viewToModel.on( 'element:figure', ( evt, data, consumable ) => {
-					const figure = data.input;
-					consumable.consume( figure, { name: true, class: 'image' } );
+				editor.data.upcastDispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
+					conversionApi.consumable.consume( data.viewItem, { name: true, class: 'image' } );
 				}, { priority: 'high' } );
 
 				editor.setData( '<figure class="image"><img src="foo.png" alt="alt text" /></figure>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '' );
 			} );
 
 			it( 'should dispatch conversion for nested elements', () => {
 				const conversionSpy = sinon.spy();
-				editor.data.viewToModel.on( 'element:figcaption', conversionSpy );
+				editor.data.upcastDispatcher.on( 'element:figcaption', conversionSpy );
 
 				editor.setData( '<figure class="image"><img src="foo.png" alt="alt text" /><figcaption></figcaption></figure>' );
 
@@ -219,34 +222,21 @@ describe( 'ImageEngine', () => {
 			it( 'should convert bare img element', () => {
 				editor.setData( '<img src="foo.png" alt="alt text" />' );
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '<image alt="alt text" src="foo.png"></image>' );
 			} );
 
 			it( 'should not convert alt attribute on non-img element', () => {
-				const data = editor.data;
-				const editing = editor.editing;
-
-				document.schema.registerItem( 'div', '$block' );
-				document.schema.allow( { name: 'div', attributes: 'alt', inside: '$root' } );
+				model.schema.register( 'div', {
+					inheritAllFrom: '$block',
+					allowAttributes: 'alt'
+				} );
 
-				buildModelConverter().for( data.modelToView, editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
-				buildViewConverter().for( data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
+				editor.conversion.elementToElement( { model: 'div', view: 'div' } );
 
 				editor.setData( '<div alt="foo"></div>' );
 
-				expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<div></div>' );
-			} );
-
-			it( 'should handle figure with two images', () => {
-				document.schema.allow( { name: '$text', inside: 'image' } );
-
-				editor.setData( '<figure class="image"><img src="foo.jpg" /><img src="bar.jpg" />abc</figure>' );
-
-				// The foo.jpg image is properly converted using figure converter. The other image was tried to
-				// be added as a child of foo.jpg and then was autohoisted.
-				expect( getModelData( document, { withoutSelection: true } ) )
-					.to.equal( '<image src="bar.jpg"></image><image src="foo.jpg">abc</image>' );
+				expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<div></div>' );
 			} );
 
 			it( 'should convert image with srcset attribute', () => {
@@ -256,7 +246,7 @@ describe( 'ImageEngine', () => {
 					'</figure>'
 				);
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '<image alt="alt text" src="foo.png" srcset="{"data":"small.png 148w, big.png 1024w"}"></image>' );
 			} );
 
@@ -267,7 +257,7 @@ describe( 'ImageEngine', () => {
 					'</figure>'
 				);
 
-				expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+				expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 					'<image alt="alt text" src="foo.png" srcset="{"data":"small.png 148w, big.png 1024w","width":"1024"}"></image>' );
 			} );
 
@@ -278,29 +268,21 @@ describe( 'ImageEngine', () => {
 					'</figure>'
 				);
 
-				expect( getModelData( document, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '<image alt="alt text" src="foo.png" srcset="{"data":"small.png 148w, big.png 1024w"}"></image>' );
 			} );
 
 			describe( 'should autohoist images', () => {
 				beforeEach( () => {
-					document.schema.registerItem( 'div', '$block' );
+					model.schema.register( 'div', { inheritAllFrom: '$block' } );
 
-					buildModelConverter()
-						.for( editor.data.modelToView, editor.editing.modelToView )
-						.fromElement( 'div' )
-						.toElement( 'div' );
-
-					buildViewConverter()
-						.for( editor.data.viewToModel )
-						.fromElement( 'div' )
-						.toElement( 'div' );
+					editor.conversion.elementToElement( { model: 'div', view: 'div' } );
 				} );
 
 				it( 'image between non-hoisted elements', () => {
 					editor.setData( '<div>foo<img src="foo.jpg" alt="foo" />bar</div>' );
 
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 						'<div>foo</div>' +
 						'<image alt="foo" src="foo.jpg"></image>' +
 						'<div>bar</div>'
@@ -310,7 +292,7 @@ describe( 'ImageEngine', () => {
 				it( 'multiple images', () => {
 					editor.setData( '<div>foo<img src="foo.jpg" alt="foo" />ba<img src="foo.jpg" alt="foo" />r</div>' );
 
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 						'<div>foo</div>' +
 						'<image alt="foo" src="foo.jpg"></image>' +
 						'<div>ba</div>' +
@@ -322,7 +304,7 @@ describe( 'ImageEngine', () => {
 				it( 'images on borders of parent', () => {
 					editor.setData( '<div><img src="foo.jpg" alt="foo" />foobar<img src="foo.jpg" alt="foo" /></div>' );
 
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 						'<image alt="foo" src="foo.jpg"></image>' +
 						'<div>foobar</div>' +
 						'<image alt="foo" src="foo.jpg"></image>'
@@ -332,18 +314,18 @@ describe( 'ImageEngine', () => {
 				it( 'images are only content of parent', () => {
 					editor.setData( '<div><img src="foo.jpg" alt="foo" /><img src="foo.jpg" alt="foo" /></div>' );
 
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 						'<image alt="foo" src="foo.jpg"></image>' +
 						'<image alt="foo" src="foo.jpg"></image>'
 					);
 				} );
 
 				it( 'deep autohoisting #1', () => {
-					document.schema.allow( { name: 'div', inside: 'div' } );
+					model.schema.extend( 'div', { allowIn: 'div' } );
 
 					editor.setData( '<div>foo<div>xx<img src="foo.jpg" alt="foo" /></div>bar</div>' );
 
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 						'<div>' +
 							'foo' +
 							'<div>' +
@@ -356,7 +338,7 @@ describe( 'ImageEngine', () => {
 				} );
 
 				it( 'deep autohoisting #2', () => {
-					document.schema.allow( { name: 'div', inside: 'div' } );
+					model.schema.extend( 'div', { allowIn: 'div' } );
 
 					editor.setData(
 						'<div>x</div>' +
@@ -364,31 +346,30 @@ describe( 'ImageEngine', () => {
 						'<div>y</div>'
 					);
 
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal(
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 						'<div>x</div><image alt="foo" src="foo.jpg"></image><div>y</div>'
 					);
 				} );
 
 				it( 'should not break a limiting element', () => {
-					document.schema.registerItem( 'limit', '$block' );
-					document.schema.allow( { name: 'div', inside: 'limit' } );
-					document.schema.limits.add( 'limit' );
-
-					buildModelConverter()
-						.for( editor.data.modelToView, editor.editing.modelToView ).fromElement( 'limit' ).toElement( 'limit' );
+					model.schema.register( 'limit', {
+						inheritAllFrom: '$block',
+						isLimit: true
+					} );
+					model.schema.extend( 'div', { allowIn: 'limit' } );
 
-					buildViewConverter().for( editor.data.viewToModel ).fromElement( 'limit' ).toElement( 'limit' );
+					editor.conversion.elementToElement( { model: 'limit', view: 'limit' } );
 
 					editor.setData( '<limit><div>foo<img src="foo.jpg" alt="foo" />bar</div></limit>' );
 
 					// <limit> element does not have converters so it is not converted.
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<limit><div>foobar</div></limit>' );
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<limit><div>foobar</div></limit>' );
 				} );
 
 				it( 'should not convert and autohoist image element without src attribute (which is not allowed by schema)', () => {
 					editor.setData( '<div>foo<img alt="foo" />bar</div>' );
 
-					expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<div>foobar</div>' );
+					expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<div>foobar</div>' );
 				} );
 			} );
 		} );
@@ -397,15 +378,15 @@ describe( 'ImageEngine', () => {
 	describe( 'conversion in editing pipeline', () => {
 		describe( 'model to view', () => {
 			it( 'should convert', () => {
-				setModelData( document, '<image src="foo.png" alt="alt text"></image>' );
+				setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false"><img alt="alt text" src="foo.png"></img></figure>'
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false"><img alt="alt text" src="foo.png"></img></figure>'
 				);
 			} );
 
 			it( 'converted element should be widgetized', () => {
-				setModelData( document, '<image src="foo.png" alt="alt text"></image>' );
+				setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
 				const figure = viewDocument.getRoot().getChild( 0 );
 
 				expect( figure.name ).to.equal( 'figure' );
@@ -413,70 +394,64 @@ describe( 'ImageEngine', () => {
 			} );
 
 			it( 'should convert attribute change', () => {
-				setModelData( document, '<image src="foo.png" alt="alt text"></image>' );
+				setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
 				const image = document.getRoot().getChild( 0 );
 
-				document.enqueueChanges( () => {
-					const batch = document.batch();
-
-					batch.setAttribute( image, 'alt', 'new text' );
+				model.change( writer => {
+					writer.setAttribute( 'alt', 'new text', image );
 				} );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false"><img alt="new text" src="foo.png"></img></figure>'
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false"><img alt="new text" src="foo.png"></img></figure>'
 				);
 			} );
 
 			it( 'should convert attribute removal', () => {
-				setModelData( document, '<image src="foo.png" alt="alt text"></image>' );
+				setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
 				const image = document.getRoot().getChild( 0 );
 
-				document.enqueueChanges( () => {
-					const batch = document.batch();
-
-					batch.removeAttribute( image, 'alt' );
+				model.change( writer => {
+					writer.removeAttribute( 'alt', image );
 				} );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) )
-					.to.equal( '<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>' );
+				expect( getViewData( view, { withoutSelection: true } ) )
+					.to.equal( '<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>' );
 			} );
 
 			it( 'should not convert change if is already consumed', () => {
-				setModelData( document, '<image src="foo.png" alt="alt text"></image>' );
+				setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
 				const image = document.getRoot().getChild( 0 );
 
-				editor.editing.modelToView.on( 'removeAttribute:alt:image', ( evt, data, consumable ) => {
-					consumable.consume( data.item, 'removeAttribute:alt' );
+				editor.editing.downcastDispatcher.on( 'attribute:alt:image', ( evt, data, conversionApi ) => {
+					conversionApi.consumable.consume( data.item, 'attribute:alt' );
 				}, { priority: 'high' } );
 
-				document.enqueueChanges( () => {
-					const batch = document.batch();
-
-					batch.removeAttribute( image, 'alt' );
+				model.change( writer => {
+					writer.removeAttribute( 'alt', image );
 				} );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false"><img alt="alt text" src="foo.png"></img></figure>'
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false"><img alt="alt text" src="foo.png"></img></figure>'
 				);
 			} );
 
 			it( 'should convert srcset attribute to srcset and sizes', () => {
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'alt="alt text" ' +
 						'srcset=\'{ "data":"small.png 148w, big.png 1024w" }\'>' +
 					'</image>' );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false">' +
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img alt="alt text" sizes="100vw" src="foo.png" srcset="small.png 148w, big.png 1024w"></img>' +
 					'</figure>'
 				);
 			} );
 
 			it( 'should not convert srcset attribute if has wrong data', () => {
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'alt="alt text" ' +
@@ -484,53 +459,49 @@ describe( 'ImageEngine', () => {
 					'</image>' );
 
 				const image = document.getRoot().getChild( 0 );
-				document.enqueueChanges( () => {
-					const batch = document.batch();
-
-					batch.removeAttribute( image, 'srcset' );
+				model.change( writer => {
+					writer.removeAttribute( 'srcset', image );
 				} );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false">' +
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img alt="alt text" src="foo.png"></img>' +
 					'</figure>'
 				);
 			} );
 
 			it( 'should convert srcset attribute to srcset, width and sizes', () => {
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'alt="alt text" ' +
 						'srcset=\'{ "data":"small.png 148w, big.png 1024w", "width":"1024" }\'>' +
 					'</image>' );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false">' +
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img alt="alt text" sizes="100vw" src="foo.png" srcset="small.png 148w, big.png 1024w" width="1024"></img>' +
 					'</figure>'
 				);
 			} );
 
 			it( 'should remove sizes and srcsset attribute when srcset attribute is removed from model', () => {
-				setModelData( document, '<image src="foo.png" srcset=\'{ "data": "small.png 148w, big.png 1024w" }\'></image>' );
+				setModelData( model, '<image src="foo.png" srcset=\'{ "data": "small.png 148w, big.png 1024w" }\'></image>' );
 				const image = document.getRoot().getChild( 0 );
 
-				document.enqueueChanges( () => {
-					const batch = document.batch();
-
-					batch.removeAttribute( image, 'srcset' );
+				model.change( writer => {
+					writer.removeAttribute( 'srcset', image );
 				} );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false">' +
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src="foo.png"></img>' +
 					'</figure>'
 				);
 			} );
 
 			it( 'should remove width, sizes and srcsset attribute when srcset attribute is removed from model', () => {
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'srcset=\'{ "data": "small.png 148w, big.png 1024w", "width": "1024" }\'>' +
@@ -538,29 +509,25 @@ describe( 'ImageEngine', () => {
 				);
 				const image = document.getRoot().getChild( 0 );
 
-				document.enqueueChanges( () => {
-					const batch = document.batch();
-
-					batch.removeAttribute( image, 'srcset' );
+				model.change( writer => {
+					writer.removeAttribute( 'srcset', image );
 				} );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false">' +
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
 					'<img src="foo.png"></img>' +
 					'</figure>'
 				);
 			} );
 
 			it( 'should not convert srcset attribute if is already consumed', () => {
-				editor.editing.modelToView.on( 'addAttribute:srcset:image', ( evt, data, consumable ) => {
-					const parts = evt.name.split( ':' );
-					const consumableType = parts[ 0 ] + ':' + parts[ 1 ];
+				editor.editing.downcastDispatcher.on( 'attribute:srcset:image', ( evt, data, conversionApi ) => {
 					const modelImage = data.item;
 
-					consumable.consume( modelImage, consumableType );
+					conversionApi.consumable.consume( modelImage, evt.name );
 				}, { priority: 'high' } );
 
-				setModelData( document,
+				setModelData( model,
 					'<image ' +
 						'src="foo.png" ' +
 						'alt="alt text" ' +
@@ -568,8 +535,8 @@ describe( 'ImageEngine', () => {
 					'</image>'
 				);
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false">' +
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img alt="alt text" src="foo.png"></img>' +
 					'</figure>'
 				);

+ 17 - 10
packages/ckeditor5-image/tests/image/ui/utils.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -14,7 +14,7 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { repositionContextualBalloon, getBalloonPositionData } from '../../../src/image/ui/utils';
 
 describe( 'Utils', () => {
-	let editor, doc, editingView, balloon, editorElement;
+	let editor, editingView, balloon, editorElement;
 
 	beforeEach( () => {
 		editorElement = global.document.createElement( 'div' );
@@ -26,7 +26,6 @@ describe( 'Utils', () => {
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				doc = editor.document;
 				editingView = editor.editing.view;
 				balloon = editor.plugins.get( 'ContextualBalloon' );
 			} );
@@ -53,14 +52,18 @@ describe( 'Utils', () => {
 				}
 			} );
 
-			setData( doc, '[<image src=""></image>]' );
+			setData( editor.model, '[<image src=""></image>]' );
 			repositionContextualBalloon( editor );
 
 			sinon.assert.calledWithExactly( spy, {
-				target: editingView.domConverter.viewToDom( editingView.selection.getSelectedElement() ),
+				target: editingView.domConverter.viewToDom( editingView.document.selection.getSelectedElement() ),
 				positions: [
 					defaultPositions.northArrowSouth,
-					defaultPositions.southArrowNorth
+					defaultPositions.northArrowSouthWest,
+					defaultPositions.northArrowSouthEast,
+					defaultPositions.southArrowNorth,
+					defaultPositions.southArrowNorthWest,
+					defaultPositions.southArrowNorthEast
 				]
 			} );
 		} );
@@ -68,7 +71,7 @@ describe( 'Utils', () => {
 		it( 'should not engage with no image is selected', () => {
 			const spy = sinon.spy( balloon, 'updatePosition' );
 
-			setData( doc, '<paragraph>foo</paragraph>' );
+			setData( editor.model, '<paragraph>foo</paragraph>' );
 
 			repositionContextualBalloon( editor );
 			sinon.assert.notCalled( spy );
@@ -79,14 +82,18 @@ describe( 'Utils', () => {
 		it( 'returns the position data', () => {
 			const defaultPositions = BalloonPanelView.defaultPositions;
 
-			setData( doc, '[<image src=""></image>]' );
+			setData( editor.model, '[<image src=""></image>]' );
 			const data = getBalloonPositionData( editor );
 
 			expect( data ).to.deep.equal( {
-				target: editingView.domConverter.viewToDom( editingView.selection.getSelectedElement() ),
+				target: editingView.domConverter.viewToDom( editingView.document.selection.getSelectedElement() ),
 				positions: [
 					defaultPositions.northArrowSouth,
-					defaultPositions.southArrowNorth
+					defaultPositions.northArrowSouthWest,
+					defaultPositions.northArrowSouthEast,
+					defaultPositions.southArrowNorth,
+					defaultPositions.southArrowNorthWest,
+					defaultPositions.southArrowNorthEast
 				]
 			} );
 		} );

+ 11 - 8
packages/ckeditor5-image/tests/image/utils.js

@@ -1,23 +1,26 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
 import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection';
 import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment';
+import ViewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
+import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
 import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import { toImageWidget, isImageWidget, isImageWidgetSelected, isImage } from '../../src/image/utils';
 import { isWidget, getLabel } from '@ckeditor/ckeditor5-widget/src/utils';
 
 describe( 'image widget utils', () => {
-	let element, image;
+	let element, image, writer;
 
 	beforeEach( () => {
+		writer = new ViewWriter( new ViewDocument() );
 		image = new ViewElement( 'img' );
 		element = new ViewElement( 'figure', null, image );
-		toImageWidget( element, 'image widget' );
+		toImageWidget( element, writer, 'image widget' );
 	} );
 
 	describe( 'toImageWidget()', () => {
@@ -30,12 +33,12 @@ describe( 'image widget utils', () => {
 		} );
 
 		it( 'should set element\'s label combined with alt attribute', () => {
-			image.setAttribute( 'alt', 'foo bar baz' );
+			writer.setAttribute( 'alt', 'foo bar baz', image );
 			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' );
+			writer.setAttribute( 'alt', 'foo bar baz', image );
 
 			expect( getLabel( element ) ).to.equal( 'foo bar baz image widget' );
 			expect( getLabel( element ) ).to.equal( 'foo bar baz image widget' );
@@ -59,7 +62,7 @@ describe( 'image widget utils', () => {
 			// We need to create a container for the element to be able to create a Range on this element.
 			frag = new ViewDocumentFragment( [ element ] );
 
-			const selection = new ViewSelection( [ ViewRange.createOn( element ) ] );
+			const selection = new ViewSelection( element, 'on' );
 
 			expect( isImageWidgetSelected( selection ) ).to.be.true;
 		} );
@@ -70,7 +73,7 @@ describe( 'image widget utils', () => {
 			// We need to create a container for the element to be able to create a Range on this element.
 			frag = new ViewDocumentFragment( [ notWidgetizedElement ] );
 
-			const selection = new ViewSelection( [ ViewRange.createOn( notWidgetizedElement ) ] );
+			const selection = new ViewSelection( notWidgetizedElement, 'on' );
 
 			expect( isImageWidgetSelected( selection ) ).to.be.false;
 		} );
@@ -80,7 +83,7 @@ describe( 'image widget utils', () => {
 
 			frag = new ViewDocumentFragment( [ element, notWidgetizedElement ] );
 
-			const selection = new ViewSelection( [ ViewRange.createIn( frag ) ] );
+			const selection = new ViewSelection( ViewRange.createIn( frag ) );
 
 			expect( isImageWidgetSelected( selection ) ).to.be.false;
 		} );

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,7 +7,7 @@
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ImageCaption from '../src/imagecaption';
-import ImageCaptionEngine from '../src/imagecaption/imagecaptionengine';
+import ImageCaptionEditing from '../src/imagecaption/imagecaptionediting';
 
 describe( 'ImageCaption', () => {
 	let editor;
@@ -29,7 +29,7 @@ describe( 'ImageCaption', () => {
 		expect( editor.plugins.get( ImageCaption ) ).to.instanceOf( ImageCaption );
 	} );
 
-	it( 'should load ImageCaptionEngine plugin', () => {
-		expect( editor.plugins.get( ImageCaptionEngine ) ).to.instanceOf( ImageCaptionEngine );
+	it( 'should load ImageCaptionEditing plugin', () => {
+		expect( editor.plugins.get( ImageCaptionEditing ) ).to.instanceOf( ImageCaptionEditing );
 	} );
 } );

+ 142 - 161
packages/ckeditor5-image/tests/imagecaption/imagecaptionengine.js → packages/ckeditor5-image/tests/imagecaption/imagecaptionediting.js

@@ -1,95 +1,90 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 
-import ImageCaptionEngine from '../../src/imagecaption/imagecaptionengine';
-import ImageEngine from '../../src/image/imageengine';
-import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
+import ImageCaptionEditing from '../../src/imagecaption/imagecaptionediting';
+import ImageEditing from '../../src/image/imageediting';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 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 { 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, doc, viewDocument;
+describe( 'ImageCaptionEditing', () => {
+	let editor, model, doc, view;
 
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ ImageCaptionEngine, ImageEngine, UndoEngine, Paragraph ]
+				plugins: [ ImageCaptionEditing, ImageEditing, UndoEditing, Paragraph ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				doc = editor.document;
-				viewDocument = editor.editing.view;
-				doc.schema.registerItem( 'widget' );
-				doc.schema.allow( { name: 'widget', inside: '$root' } );
-				doc.schema.allow( { name: 'caption', inside: 'widget' } );
-				doc.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' );
+				model = editor.model;
+				doc = model.document;
+				view = editor.editing.view;
+				model.schema.register( 'widget' );
+				model.schema.extend( 'widget', { allowIn: '$root' } );
+				model.schema.extend( 'caption', { allowIn: 'widget' } );
+				model.schema.extend( '$text', { allowIn: 'widget' } );
+
+				editor.conversion.elementToElement( {
+					model: 'widget',
+					view: 'widget'
+				} );
 			} );
 	} );
 
 	it( 'should be loaded', () => {
-		expect( editor.plugins.get( ImageCaptionEngine ) ).to.be.instanceOf( ImageCaptionEngine );
+		expect( editor.plugins.get( ImageCaptionEditing ) ).to.be.instanceOf( ImageCaptionEditing );
 	} );
 
 	it( 'should set proper schema rules', () => {
-		expect( doc.schema.check( { name: 'caption', iniside: 'image' } ) ).to.be.true;
-		expect( doc.schema.check( { name: '$inline', inside: 'caption' } ) ).to.be.true;
-		expect( doc.schema.itemExtends( 'caption', '$block' ) ).to.be.true;
-		expect( doc.schema.limits.has( 'caption' ) );
+		expect( model.schema.checkChild( [ '$root', 'image' ], 'caption' ) ).to.be.true;
+		expect( model.schema.checkChild( [ '$root', 'image', 'caption' ], '$text' ) ).to.be.true;
+		expect( model.schema.isLimit( 'caption' ) ).to.be.true;
+
+		expect( model.schema.checkChild( [ '$root', 'image', 'caption' ], 'caption' ) ).to.be.false;
+
+		model.schema.extend( '$block', { allowAttributes: 'aligmnent' } );
+		expect( model.schema.checkAttribute( [ '$root', 'image', 'caption' ], 'alignment' ) ).to.be.false;
 	} );
 
 	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>' );
+				editor.setData( '<figure class="image"><img src="foo.png" /><figcaption>foo bar</figcaption></figure>' );
 
-				expect( getModelData( doc, { withoutSelection: true } ) )
+				expect( getModelData( model, { 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>' );
+				editor.setData( '<figure class="image"><img src="foo.png" /></figure>' );
 
-				expect( getModelData( doc, { withoutSelection: true } ) )
+				expect( getModelData( model, { 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( doc, { withoutSelection: true } ) )
+				expect( getModelData( model, { withoutSelection: true } ) )
 					.to.equal( '<widget>foobar</widget>' );
 			} );
 		} );
 
 		describe( 'model to view', () => {
 			it( 'should convert caption element to figcaption', () => {
-				setModelData( doc, '<image src="img.png"><caption>Foo bar baz.</caption></image>' );
+				setModelData( model, '<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>'
@@ -97,13 +92,13 @@ describe( 'ImageCaptionEngine', () => {
 			} );
 
 			it( 'should not convert caption to figcaption if it\'s empty', () => {
-				setModelData( doc, '<image src="img.png"><caption></caption></image>' );
+				setModelData( model, '<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( doc, '<widget>foo bar<caption></caption></widget>' );
+				setModelData( model, '<widget>foo bar<caption></caption></widget>' );
 
 				expect( editor.getData() ).to.equal( '<widget>foo bar</widget>' );
 			} );
@@ -113,10 +108,10 @@ describe( 'ImageCaptionEngine', () => {
 	describe( 'editing pipeline', () => {
 		describe( 'model to view', () => {
 			it( 'should convert caption element to figcaption contenteditable', () => {
-				setModelData( doc, '<image src="img.png"><caption>Foo bar baz.</caption></image>' );
+				setModelData( model, '<image src="img.png"><caption>Foo bar baz.</caption></image>' );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-					'<figure class="image ck-widget" contenteditable="false">' +
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src="img.png"></img>' +
 						'<figcaption class="ck-editable" contenteditable="true" data-placeholder="Enter image caption">' +
 							'Foo bar baz.' +
@@ -126,13 +121,13 @@ describe( 'ImageCaptionEngine', () => {
 			} );
 
 			it( 'should convert caption to element with proper CSS class if it\'s empty', () => {
-				setModelData( doc, '<paragraph>foo</paragraph><image src="img.png"><caption></caption></image>' );
+				setModelData( model, '<paragraph>foo</paragraph><image src="img.png"><caption></caption></image>' );
 
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
 					'<p>foo</p>' +
-					'<figure class="image ck-widget" contenteditable="false">' +
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src="img.png"></img>' +
-						'<figcaption class="ck-placeholder ck-editable ck-hidden" ' +
+						'<figcaption class="ck-editable ck-hidden ck-placeholder" ' +
 							'contenteditable="true" data-placeholder="Enter image caption">' +
 						'</figcaption>' +
 					'</figure>'
@@ -140,47 +135,46 @@ describe( 'ImageCaptionEngine', () => {
 			} );
 
 			it( 'should not convert caption from other elements', () => {
-				setModelData( doc, '<widget>foo bar<caption></caption></widget>' );
-				expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal( '<widget>foo bar</widget>' );
+				setModelData( model, '<widget>foo bar<caption></caption></widget>' );
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<widget>foo bar</widget>' );
 			} );
 
 			it( 'should not convert when element is already consumed', () => {
-				editor.editing.modelToView.on(
+				editor.editing.downcastDispatcher.on(
 					'insert:caption',
-					( evt, data, consumable, conversionApi ) => {
-						consumable.consume( data.item, 'insert' );
+					( evt, data, conversionApi ) => {
+						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 );
+						conversionApi.writer.insert( viewPosition, viewElement );
 					},
 					{ priority: 'high' }
 				);
 
-				setModelData( doc, '<image src="img.png"><caption>Foo bar baz.</caption></image>' );
+				setModelData( model, '<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>'
+				expect( getViewData( view, { withoutSelection: true } ) ).to.equal(
+					'<figure class="ck-widget image" contenteditable="false"><img src="img.png"></img><span></span>Foo bar baz.</figure>'
 				);
 			} );
 
 			it( 'should show caption when something is inserted inside', () => {
-				setModelData( doc, '<paragraph>foo</paragraph><image src="img.png"><caption></caption></image>' );
+				setModelData( model, '<paragraph>foo</paragraph><image src="img.png"><caption></caption></image>' );
 
 				const image = doc.getRoot().getChild( 1 );
 				const caption = image.getChild( 0 );
 
-				doc.enqueueChanges( () => {
-					const batch = doc.batch();
-					batch.insert( ModelPosition.createAt( caption ), 'foo bar' );
+				model.change( writer => {
+					writer.insertText( 'foo bar', caption );
 				} );
 
-				expect( getViewData( viewDocument ) ).to.equal(
+				expect( getViewData( view ) ).to.equal(
 					'<p>{}foo</p>' +
-					'<figure class="image ck-widget" contenteditable="false">' +
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src="img.png"></img>' +
 						'<figcaption class="ck-editable" contenteditable="true" data-placeholder="Enter image caption">' +
 							'foo bar' +
@@ -190,19 +184,18 @@ describe( 'ImageCaptionEngine', () => {
 			} );
 
 			it( 'should hide when everything is removed from caption', () => {
-				setModelData( doc, '<paragraph>foo</paragraph><image src="img.png"><caption>foo bar baz</caption></image>' );
+				setModelData( model, '<paragraph>foo</paragraph><image src="img.png"><caption>foo bar baz</caption></image>' );
 
 				const image = doc.getRoot().getChild( 1 );
 				const caption = image.getChild( 0 );
 
-				doc.enqueueChanges( () => {
-					const batch = doc.batch();
-					batch.remove( ModelRange.createIn( caption ) );
+				model.change( writer => {
+					writer.remove( ModelRange.createIn( caption ) );
 				} );
 
-				expect( getViewData( viewDocument ) ).to.equal(
+				expect( getViewData( view ) ).to.equal(
 					'<p>{}foo</p>' +
-					'<figure class="image ck-widget" contenteditable="false">' +
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src="img.png"></img>' +
 						'<figcaption class="ck-editable ck-hidden ck-placeholder" ' +
 							'contenteditable="true" data-placeholder="Enter image caption">' +
@@ -212,19 +205,18 @@ describe( 'ImageCaptionEngine', () => {
 			} );
 
 			it( 'should show when not everything is removed from caption', () => {
-				setModelData( doc, '<paragraph>foo</paragraph><image src="img.png"><caption>foo bar baz</caption></image>' );
+				setModelData( model, '<paragraph>foo</paragraph><image src="img.png"><caption>foo bar baz</caption></image>' );
 
 				const image = doc.getRoot().getChild( 1 );
 				const caption = image.getChild( 0 );
 
-				doc.enqueueChanges( () => {
-					const batch = doc.batch();
-					batch.remove( ModelRange.createFromParentsAndOffsets( caption, 0, caption, 8 ) );
+				model.change( writer => {
+					writer.remove( ModelRange.createFromParentsAndOffsets( caption, 0, caption, 8 ) );
 				} );
 
-				expect( getViewData( viewDocument ) ).to.equal(
+				expect( getViewData( view ) ).to.equal(
 					'<p>{}foo</p>' +
-					'<figure class="image ck-widget" contenteditable="false">' +
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src="img.png"></img>' +
 						'<figcaption class="ck-editable" contenteditable="true" data-placeholder="Enter image caption">baz</figcaption>' +
 					'</figure>'
@@ -235,21 +227,18 @@ describe( 'ImageCaptionEngine', () => {
 
 	describe( 'inserting image to the document', () => {
 		it( 'should add caption element if image does not have it', () => {
-			const image = new ModelElement( 'image', { src: '', alt: '' } );
-			const batch = doc.batch();
-
-			doc.enqueueChanges( () => {
-				batch.insert( new ModelPosition( doc.getRoot(), [ 0 ] ), image );
+			model.change( writer => {
+				writer.insertElement( 'image', { src: '', alt: '' }, doc.getRoot() );
 			} );
 
-			expect( getModelData( doc ) ).to.equal(
+			expect( getModelData( model ) ).to.equal(
 				'[<image alt="" src=""><caption></caption></image>]<paragraph></paragraph>'
 			);
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'[<figure class="image ck-widget" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'[<figure class="ck-widget image" contenteditable="false">' +
 					'<img alt="" src=""></img>' +
-					'<figcaption class="ck-placeholder ck-editable" ' +
+					'<figcaption class="ck-editable ck-placeholder" ' +
 						'contenteditable="true" data-placeholder="Enter image caption">' +
 					'</figcaption>' +
 				'</figure>]' +
@@ -260,18 +249,17 @@ describe( 'ImageCaptionEngine', () => {
 		it( 'should not add caption element if image already have it', () => {
 			const caption = new ModelElement( 'caption', null, 'foo bar' );
 			const image = new ModelElement( 'image', { src: '', alt: '' }, caption );
-			const batch = doc.batch();
 
-			doc.enqueueChanges( () => {
-				batch.insert( new ModelPosition( doc.getRoot(), [ 0 ] ), image );
+			model.change( writer => {
+				writer.insert( image, doc.getRoot() );
 			} );
 
-			expect( getModelData( doc ) ).to.equal(
+			expect( getModelData( model ) ).to.equal(
 				'[<image alt="" src=""><caption>foo bar</caption></image>]<paragraph></paragraph>'
 			);
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'[<figure class="image ck-widget" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'[<figure class="ck-widget image" contenteditable="false">' +
 					'<img alt="" src=""></img>' +
 					'<figcaption class="ck-editable" contenteditable="true" data-placeholder="Enter image caption">' +
 						'foo bar' +
@@ -284,25 +272,24 @@ describe( 'ImageCaptionEngine', () => {
 		it( 'should not add caption element twice', () => {
 			const image = new ModelElement( 'image', { src: '', alt: '' } );
 			const caption = new ModelElement( 'caption' );
-			const batch = doc.batch();
-
-			doc.enqueueChanges( () => {
-				batch
-					// Since we are adding an empty image, this should trigger caption fixer.
-					.insert( ModelPosition.createAt( doc.getRoot() ), image )
-					// Add caption just after the image is inserted, in same batch and enqueue changes block.
-					.insert( ModelPosition.createAt( image ), caption );
+
+			model.change( writer => {
+				// Since we are adding an empty image, this should trigger caption fixer.
+				writer.insert( image, doc.getRoot() );
+
+				// Add caption just after the image is inserted, in same batch.
+				writer.insert( caption, image );
 			} );
 
 			// Check whether caption fixer added redundant caption.
-			expect( getModelData( doc ) ).to.equal(
+			expect( getModelData( model ) ).to.equal(
 				'[<image alt="" src=""><caption></caption></image>]<paragraph></paragraph>'
 			);
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'[<figure class="image ck-widget" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'[<figure class="ck-widget image" contenteditable="false">' +
 					'<img alt="" src=""></img>' +
-					'<figcaption class="ck-placeholder ck-editable" ' +
+					'<figcaption class="ck-editable ck-placeholder" ' +
 						'contenteditable="true" data-placeholder="Enter image caption"></figcaption>' +
 				'</figure>]' +
 				'<p></p>'
@@ -310,16 +297,15 @@ describe( 'ImageCaptionEngine', () => {
 		} );
 
 		it( 'should do nothing for other changes than insert', () => {
-			setModelData( doc, '<image src=""><caption>foo bar</caption></image>' );
+			setModelData( model, '<image src=""><caption>foo bar</caption></image>' );
 
 			const image = doc.getRoot().getChild( 0 );
-			const batch = doc.batch();
 
-			doc.enqueueChanges( () => {
-				batch.setAttribute( image, 'alt', 'alt text' );
+			model.change( writer => {
+				writer.setAttribute( 'alt', 'alt text', image );
 			} );
 
-			expect( getModelData( doc, { withoutSelection: true } ) ).to.equal(
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
 				'<image alt="alt text" src=""><caption>foo bar</caption></image>'
 			);
 		} );
@@ -327,26 +313,26 @@ describe( 'ImageCaptionEngine', () => {
 
 	describe( 'editing view', () => {
 		it( 'image should have empty figcaption element when is selected', () => {
-			setModelData( doc, '<paragraph>foo</paragraph>[<image src=""><caption></caption></image>]' );
+			setModelData( model, '<paragraph>foo</paragraph>[<image src=""><caption></caption></image>]' );
 
-			expect( getViewData( viewDocument ) ).to.equal(
+			expect( getViewData( view ) ).to.equal(
 				'<p>foo</p>' +
-				'[<figure class="image ck-widget" contenteditable="false">' +
+				'[<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
-					'<figcaption class="ck-placeholder ck-editable" contenteditable="true" data-placeholder="Enter image caption">' +
+					'<figcaption class="ck-editable ck-placeholder" contenteditable="true" data-placeholder="Enter image caption">' +
 					'</figcaption>' +
 				'</figure>]'
 			);
 		} );
 
 		it( 'image should have empty figcaption element with hidden class when not selected', () => {
-			setModelData( doc, '<paragraph>[]foo</paragraph><image src=""><caption></caption></image>' );
+			setModelData( model, '<paragraph>[]foo</paragraph><image src=""><caption></caption></image>' );
 
-			expect( getViewData( viewDocument ) ).to.equal(
+			expect( getViewData( view ) ).to.equal(
 				'<p>{}foo</p>' +
-				'<figure class="image ck-widget" contenteditable="false">' +
+				'<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
-					'<figcaption class="ck-placeholder ck-editable ck-hidden" ' +
+					'<figcaption class="ck-editable ck-hidden ck-placeholder" ' +
 						'contenteditable="true" data-placeholder="Enter image caption">' +
 					'</figcaption>' +
 				'</figure>'
@@ -354,11 +340,11 @@ describe( 'ImageCaptionEngine', () => {
 		} );
 
 		it( 'should not add additional figcaption if one is already present', () => {
-			setModelData( doc, '<paragraph>foo</paragraph>[<image src=""><caption>foo bar</caption></image>]' );
+			setModelData( model, '<paragraph>foo</paragraph>[<image src=""><caption>foo bar</caption></image>]' );
 
-			expect( getViewData( viewDocument ) ).to.equal(
+			expect( getViewData( view ) ).to.equal(
 				'<p>foo</p>' +
-				'[<figure class="image ck-widget" contenteditable="false">' +
+				'[<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
 					'<figcaption class="ck-editable" contenteditable="true" data-placeholder="Enter image caption">foo bar</figcaption>' +
 				'</figure>]'
@@ -366,17 +352,17 @@ describe( 'ImageCaptionEngine', () => {
 		} );
 
 		it( 'should add hidden class to figcaption when caption is empty and image is no longer selected', () => {
-			setModelData( doc, '<paragraph>foo</paragraph>[<image src=""><caption></caption></image>]' );
+			setModelData( model, '<paragraph>foo</paragraph>[<image src=""><caption></caption></image>]' );
 
-			doc.enqueueChanges( () => {
-				doc.selection.removeAllRanges();
+			model.change( writer => {
+				writer.setSelection( null );
 			} );
 
-			expect( getViewData( viewDocument ) ).to.equal(
+			expect( getViewData( view ) ).to.equal(
 				'<p>{}foo</p>' +
-				'<figure class="image ck-widget" contenteditable="false">' +
+				'<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
-					'<figcaption class="ck-placeholder ck-editable ck-hidden" ' +
+					'<figcaption class="ck-editable ck-hidden ck-placeholder" ' +
 						'contenteditable="true" data-placeholder="Enter image caption">' +
 					'</figcaption>' +
 				'</figure>'
@@ -384,14 +370,14 @@ describe( 'ImageCaptionEngine', () => {
 		} );
 
 		it( 'should not remove figcaption when selection is inside it even when it is empty', () => {
-			setModelData( doc, '<image src=""><caption>[foo bar]</caption></image>' );
+			setModelData( model, '<image src=""><caption>[foo bar]</caption></image>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( doc.selection.getFirstRange() );
+			model.change( writer => {
+				writer.remove( doc.selection.getFirstRange() );
 			} );
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
 					'<figcaption class="ck-editable ck-placeholder" contenteditable="true" data-placeholder="Enter image caption">' +
 						'[]' +
@@ -401,16 +387,16 @@ describe( 'ImageCaptionEngine', () => {
 		} );
 
 		it( 'should not remove figcaption when selection is moved from it to its image', () => {
-			setModelData( doc, '<image src=""><caption>[foo bar]</caption></image>' );
+			setModelData( model, '<image src=""><caption>[foo bar]</caption></image>' );
 			const image = doc.getRoot().getChild( 0 );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( doc.selection.getFirstRange() );
-				doc.selection.setRanges( [ ModelRange.createOn( image ) ] );
+			model.change( writer => {
+				writer.remove( doc.selection.getFirstRange() );
+				writer.setSelection( ModelRange.createOn( image ) );
 			} );
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'[<figure class="image ck-widget" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'[<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
 					'<figcaption class="ck-editable ck-placeholder" ' +
 						'contenteditable="true" data-placeholder="Enter image caption"></figcaption>' +
@@ -419,21 +405,21 @@ describe( 'ImageCaptionEngine', () => {
 		} );
 
 		it( 'should not remove figcaption when selection is moved from it to other image', () => {
-			setModelData( doc, '<image src=""><caption>[foo bar]</caption></image><image src=""><caption></caption></image>' );
+			setModelData( model, '<image src=""><caption>[foo bar]</caption></image><image src=""><caption></caption></image>' );
 			const image = doc.getRoot().getChild( 1 );
 
-			doc.enqueueChanges( () => {
-				doc.selection.setRanges( [ ModelRange.createOn( image ) ] );
+			model.change( writer => {
+				writer.setSelection( ModelRange.createOn( image ) );
 			} );
 
-			expect( getViewData( viewDocument ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false">' +
+			expect( getViewData( view ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
 					'<figcaption class="ck-editable" contenteditable="true" data-placeholder="Enter image caption">foo bar</figcaption>' +
 				'</figure>' +
-				'[<figure class="image ck-widget" contenteditable="false">' +
+				'[<figure class="ck-widget image" contenteditable="false">' +
 					'<img src=""></img>' +
-					'<figcaption class="ck-placeholder ck-editable" ' +
+					'<figcaption class="ck-editable ck-placeholder" ' +
 						'contenteditable="true" data-placeholder="Enter image caption"></figcaption>' +
 				'</figure>]'
 			);
@@ -441,24 +427,22 @@ describe( 'ImageCaptionEngine', () => {
 
 		describe( 'undo/redo integration', () => {
 			it( 'should create view element after redo', () => {
-				setModelData( doc, '<paragraph>foo</paragraph><image src=""><caption>[foo bar baz]</caption></image>' );
+				setModelData( model, '<paragraph>foo</paragraph><image src=""><caption>[foo bar baz]</caption></image>' );
 
 				const modelRoot = doc.getRoot();
 				const modelImage = modelRoot.getChild( 1 );
 				const modelCaption = modelImage.getChild( 0 );
 
 				// Remove text and selection from caption.
-				doc.enqueueChanges( () => {
-					const batch = doc.batch();
-
-					batch.remove( ModelRange.createIn( modelCaption ) );
-					doc.selection.removeAllRanges();
+				model.change( writer => {
+					writer.remove( ModelRange.createIn( modelCaption ) );
+					writer.setSelection( null );
 				} );
 
 				// Check if there is no figcaption in the view.
-				expect( getViewData( viewDocument ) ).to.equal(
+				expect( getViewData( view ) ).to.equal(
 					'<p>{}foo</p>' +
-					'<figure class="image ck-widget" contenteditable="false">' +
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src=""></img>' +
 						'<figcaption class="ck-editable ck-hidden ck-placeholder" ' +
 							'contenteditable="true" data-placeholder="Enter image caption">' +
@@ -469,9 +453,9 @@ describe( 'ImageCaptionEngine', () => {
 				editor.execute( 'undo' );
 
 				// Check if figcaption is back with contents.
-				expect( getViewData( viewDocument ) ).to.equal(
+				expect( getViewData( view ) ).to.equal(
 					'<p>foo</p>' +
-					'<figure class="image ck-widget" contenteditable="false">' +
+					'<figure class="ck-widget image" contenteditable="false">' +
 						'<img src=""></img>' +
 						'<figcaption class="ck-editable" contenteditable="true" data-placeholder="Enter image caption">' +
 							'{foo bar baz}' +
@@ -481,22 +465,19 @@ describe( 'ImageCaptionEngine', () => {
 			} );
 
 			it( 'undo should work after inserting the image', () => {
-				const image = new ModelElement( 'image' );
-				image.setAttribute( 'src', '/foo.png' );
-
-				setModelData( doc, '<paragraph>foo[]</paragraph>' );
+				setModelData( model, '<paragraph>foo[]</paragraph>' );
 
-				doc.enqueueChanges( () => {
-					const batch = doc.batch();
+				model.change( writer => {
+					const image = writer.createElement( 'image', { src: '/foo.png' } );
 
-					batch.insert( ModelPosition.createAt( doc.getRoot() ), image );
+					writer.insert( image, doc.getRoot() );
 				} );
 
-				expect( getModelData( doc ) ).to.equal( '<image src="/foo.png"><caption></caption></image><paragraph>foo[]</paragraph>' );
+				expect( getModelData( model ) ).to.equal( '<image src="/foo.png"><caption></caption></image><paragraph>foo[]</paragraph>' );
 
 				editor.execute( 'undo' );
 
-				expect( getModelData( doc ) ).to.equal( '<paragraph>foo[]</paragraph>' );
+				expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph>' );
 			} );
 		} );
 	} );

+ 11 - 7
packages/ckeditor5-image/tests/imagecaption/utils.js

@@ -1,9 +1,9 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
-import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
+import View from '@ckeditor/ckeditor5-engine/src/view/view';
 import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
 import {
@@ -15,12 +15,16 @@ import {
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 
 describe( 'image captioning utils', () => {
-	let element, document;
+	let element, view, document;
 
 	beforeEach( () => {
-		document = new ViewDocument();
-		const creator = captionElementCreator( document, 'placeholder text' );
-		element = creator();
+		view = new View();
+		document = view.document;
+		const creator = captionElementCreator( view, 'placeholder text' );
+
+		view.change( writer => {
+			element = creator( writer );
+		} );
 	} );
 
 	describe( 'captionElementCreator', () => {
@@ -42,7 +46,7 @@ describe( 'image captioning utils', () => {
 
 		it( 'should return false for other elements', () => {
 			const editable = new ViewEditableElement( 'figcaption', { contenteditable: true } );
-			editable.document = document;
+			editable._document = document;
 
 			expect( isCaption( editable ) ).to.be.false;
 		} );

+ 8 - 53
packages/ckeditor5-image/tests/imagestyle.js

@@ -1,21 +1,16 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ImageStyle from '../src/imagestyle';
-import ImageStyleEngine from '../src/imagestyle/imagestyleengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import ImageStyleEditing from '../src/imagestyle/imagestyleediting';
+import ImageStyleUI from '../src/imagestyle/imagestyleui';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 describe( 'ImageStyle', () => {
 	let editor;
-	const styles = [
-		{ name: 'style 1', title: 'Style 1 title', icon: 'style1-icon', isDefault: true },
-		{ name: 'style 2', title: 'Style 2 title', icon: 'style2-icon', cssClass: 'style2-class' },
-		{ name: 'style 3', title: 'Style 3 title', icon: 'style3-icon', cssClass: 'style3-class' }
-	];
 
 	beforeEach( () => {
 		const editorElement = global.document.createElement( 'div' );
@@ -23,10 +18,7 @@ describe( 'ImageStyle', () => {
 
 		return ClassicTestEditor
 			.create( editorElement, {
-				plugins: [ ImageStyle ],
-				image: {
-					styles
-				}
+				plugins: [ ImageStyle ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
@@ -41,48 +33,11 @@ describe( 'ImageStyle', () => {
 		expect( editor.plugins.get( ImageStyle ) ).to.be.instanceOf( ImageStyle );
 	} );
 
-	it( 'should load ImageStyleEngine plugin', () => {
-		expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine );
+	it( 'should load ImageStyleEditing plugin', () => {
+		expect( editor.plugins.get( ImageStyleEditing ) ).to.be.instanceOf( ImageStyleEditing );
 	} );
 
-	it( 'should register buttons for each style', () => {
-		const spy = sinon.spy( editor, 'execute' );
-
-		for ( const style of styles ) {
-			const command = editor.commands.get( style.name );
-			const buttonView = editor.ui.componentFactory.create( style.name );
-
-			expect( buttonView ).to.be.instanceOf( ButtonView );
-			expect( buttonView.label ).to.equal( style.title );
-			expect( buttonView.icon ).to.equal( style.icon );
-
-			command.isEnabled = true;
-			expect( buttonView.isEnabled ).to.be.true;
-			command.isEnabled = false;
-			expect( buttonView.isEnabled ).to.be.false;
-
-			buttonView.fire( 'execute' );
-			sinon.assert.calledWithExactly( editor.execute, style.name );
-
-			spy.reset();
-		}
-	} );
-
-	it( 'should not add buttons to image toolbar if configuration is present', () => {
-		const editorElement = global.document.createElement( 'div' );
-		global.document.body.appendChild( editorElement );
-
-		return ClassicTestEditor
-			.create( editorElement, {
-				plugins: [ ImageStyle ],
-				image: {
-					styles,
-					toolbar: [ 'foo', 'bar' ]
-				}
-			} )
-			.then( newEditor => {
-				expect( newEditor.config.get( 'image.toolbar' ) ).to.eql( [ 'foo', 'bar' ] );
-				newEditor.destroy();
-			} );
+	it( 'should load ImageStyleUI plugin', () => {
+		expect( editor.plugins.get( ImageStyleUI ) ).to.be.instanceOf( ImageStyleUI );
 	} );
 } );

+ 61 - 53
packages/ckeditor5-image/tests/imagestyle/imagestylecommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -11,115 +11,123 @@ describe( 'ImageStyleCommand', () => {
 	const defaultStyle = { name: 'defaultStyle', title: 'foo bar', icon: 'icon-1', isDefault: true };
 	const otherStyle = { name: 'otherStyle', title: 'baz', icon: 'icon-2', className: 'other-class-name' };
 
-	let document, defaultStyleCommand, otherStyleCommand;
+	let model, command;
 
 	beforeEach( () => {
 		return ModelTestEditor.create()
 			.then( newEditor => {
-				document = newEditor.document;
-				defaultStyleCommand = new ImageStyleCommand( newEditor, defaultStyle );
-				otherStyleCommand = new ImageStyleCommand( newEditor, otherStyle );
+				model = newEditor.model;
+				command = new ImageStyleCommand( newEditor, [ defaultStyle, otherStyle ] );
 
-				document.schema.registerItem( 'p', '$block' );
+				model.schema.register( 'p', { inheritAllFrom: '$block' } );
 
-				document.schema.registerItem( 'image' );
-				document.schema.objects.add( 'image' );
-				document.schema.allow( { name: 'image', inside: '$root' } );
-				document.schema.allow( { name: 'image', inside: '$root', attributes: [ 'imageStyle' ] } );
+				model.schema.register( 'image', {
+					isObject: true,
+					isBlock: true,
+					allowWhere: '$block',
+					allowAttributes: 'imageStyle'
+				} );
 			} );
 	} );
 
 	it( 'command value should be false if no image is selected', () => {
-		setData( document, '[]<image></image>' );
+		setData( model, '[]<image></image>' );
 
-		expect( defaultStyleCommand.value ).to.be.false;
-		expect( otherStyleCommand.value ).to.be.false;
+		expect( command.value ).to.be.false;
 	} );
 
 	it( 'should match default style if no imageStyle attribute is present', () => {
-		setData( document, '[<image></image>]' );
+		setData( model, '[<image></image>]' );
 
-		expect( defaultStyleCommand.value ).to.be.true;
-		expect( otherStyleCommand.value ).to.be.false;
+		expect( command.value ).to.equal( 'defaultStyle' );
 	} );
 
 	it( 'proper command should have true value when imageStyle attribute is present', () => {
-		setData( document, '[<image imageStyle="otherStyle"></image>]' );
+		setData( model, '[<image imageStyle="otherStyle"></image>]' );
 
-		expect( defaultStyleCommand.value ).to.be.false;
-		expect( otherStyleCommand.value ).to.be.true;
+		expect( command.value ).to.equal( 'otherStyle' );
 	} );
 
 	it( 'should have false value if style does not match', () => {
-		setData( document, '[<image imageStyle="foo"></image>]' );
+		setData( model, '[<image imageStyle="foo"></image>]' );
 
-		expect( defaultStyleCommand.value ).to.be.false;
-		expect( otherStyleCommand.value ).to.be.false;
+		expect( command.value ).to.be.false;
 	} );
 
 	it( 'should set proper value when executed', () => {
-		setData( document, '[<image></image>]' );
+		setData( model, '[<image></image>]' );
 
-		otherStyleCommand.execute();
+		command.execute( { value: 'otherStyle' } );
 
-		expect( getData( document ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
+		expect( getData( model ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
 	} );
 
 	it( 'should do nothing when attribute already present', () => {
-		setData( document, '[<image imageStyle="otherStyle"></image>]' );
+		setData( model, '[<image imageStyle="otherStyle"></image>]' );
 
-		otherStyleCommand.execute();
+		command.execute( { value: 'otherStyle' } );
 
-		expect( getData( document ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
+		expect( getData( model ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
 	} );
 
-	it( 'should allow to provide batch instance', () => {
-		const batch = document.batch();
-		const spy = sinon.spy( batch, 'setAttribute' );
+	it( 'should do nothing attribute is not known', () => {
+		setData( model, '[<image imageStyle="otherStyle"></image>]' );
 
-		setData( document, '[<image></image>]' );
+		command.execute( { value: 'someNotExistingStyle' } );
 
-		otherStyleCommand.execute( { batch } );
+		expect( getData( model ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
+	} );
+
+	it( 'should do nothing when value is not passed', () => {
+		setData( model, '[<image imageStyle="otherStyle"></image>]' );
+
+		command.execute();
+
+		expect( getData( model ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
+	} );
+
+	it( 'should use parent batch', () => {
+		setData( model, '[<image></image>]' );
+
+		model.change( writer => {
+			expect( writer.batch.deltas ).to.length( 0 );
+
+			command.execute( { value: 'otherStyle' } );
 
-		expect( getData( document ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
-		sinon.assert.calledOnce( spy );
+			expect( writer.batch.deltas ).to.length.above( 0 );
+		} );
 	} );
 
 	it( 'should be enabled on image element', () => {
-		setData( document, '[<image></image>]' );
+		setData( model, '[<image></image>]' );
 
-		expect( defaultStyleCommand.isEnabled ).to.be.true;
-		expect( otherStyleCommand.isEnabled ).to.be.true;
+		expect( command.isEnabled ).to.be.true;
 	} );
 
 	it( 'should be disabled when not placed on image', () => {
-		setData( document, '[<p></p>]' );
+		setData( model, '[<p></p>]' );
 
-		expect( defaultStyleCommand.isEnabled ).to.be.false;
-		expect( otherStyleCommand.isEnabled ).to.be.false;
+		expect( command.isEnabled ).to.be.false;
 	} );
 
 	it( 'should be disabled when not placed directly on image', () => {
-		setData( document, '[<p></p><image></image>]' );
+		setData( model, '[<p></p><image></image>]' );
 
-		expect( defaultStyleCommand.isEnabled ).to.be.false;
-		expect( otherStyleCommand.isEnabled ).to.be.false;
+		expect( command.isEnabled ).to.be.false;
 	} );
 
 	it( 'default style should be active after executing it after another style', () => {
-		setData( document, '[<image></image>]' );
+		setData( model, '[<image></image>]' );
 
-		expect( defaultStyleCommand.value ).to.be.true;
-		expect( otherStyleCommand.value ).to.be.false;
+		expect( command.value ).to.equal( 'defaultStyle' );
 
-		otherStyleCommand.execute();
+		command.execute( { value: 'otherStyle' } );
 
-		expect( getData( document ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
+		expect( getData( model ) ).to.equal( '[<image imageStyle="otherStyle"></image>]' );
 
-		defaultStyleCommand.execute();
+		command.execute( { value: 'defaultStyle' } );
 
-		expect( getData( document ) ).to.equal( '[<image></image>]' );
-		expect( defaultStyleCommand.value ).to.be.true;
-		expect( otherStyleCommand.value ).to.be.false;
+		expect( getData( model ) ).to.equal( '[<image></image>]' );
+		expect( command.value ).to.equal( 'defaultStyle' );
 	} );
 } );

+ 305 - 0
packages/ckeditor5-image/tests/imagestyle/imagestyleediting.js

@@ -0,0 +1,305 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import ImageStyleEditing from '../../src/imagestyle/imagestyleediting';
+import ImageEditing from '../../src/image/imageediting';
+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';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'ImageStyleEditing', () => {
+	let editor, model, document, viewDocument;
+
+	afterEach( () => {
+		editor.destroy();
+	} );
+
+	describe( 'plugin', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEditing ],
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+				} );
+		} );
+
+		it( 'should be loaded', () => {
+			expect( editor.plugins.get( ImageStyleEditing ) ).to.be.instanceOf( ImageStyleEditing );
+		} );
+
+		it( 'should load image editing', () => {
+			expect( editor.plugins.get( ImageEditing ) ).to.be.instanceOf( ImageEditing );
+		} );
+	} );
+
+	describe( 'init', () => {
+		beforeEach( () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEditing ],
+					image: {
+						styles: [
+							{ name: 'fullStyle', title: 'foo', icon: 'object-center', isDefault: true },
+							{ name: 'sideStyle', title: 'bar', icon: 'object-right', className: 'side-class' },
+							{ name: 'dummyStyle', title: 'baz', icon: 'object-dummy', className: 'dummy-class' },
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+					document = model.document;
+					viewDocument = editor.editing.view;
+				} );
+		} );
+
+		it( 'should define image.styles config', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEditing ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'full', 'side' ] );
+				} );
+		} );
+
+		it( 'should set schema rules for image style', () => {
+			const schema = model.schema;
+
+			expect( schema.checkAttribute( [ '$root', 'image' ], 'imageStyle' ) ).to.be.true;
+		} );
+
+		it( 'should register a command', () => {
+			expect( editor.commands.get( 'imageStyle' ) ).to.be.instanceOf( ImageStyleCommand );
+		} );
+
+		it( 'should convert from view to model', () => {
+			editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
+
+			expect( getModelData( model, { withoutSelection: true } ) )
+				.to.equal( '<image imageStyle="sideStyle" src="foo.png"></image>' );
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image side-class" contenteditable="false">' +
+					'<img src="foo.png"></img>' +
+				'</figure>' );
+		} );
+
+		it( 'should not convert from view to model if class is not defined', () => {
+			editor.setData( '<figure class="image foo-bar"><img src="foo.png" /></figure>' );
+
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should not convert from view to model when not in image figure', () => {
+			editor.setData( '<figure class="side-class"></figure>' );
+
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal( '' );
+		} );
+
+		it( 'should not convert from view to model if schema prevents it', () => {
+			model.schema.addAttributeCheck( ( ctx, attributeName ) => {
+				if ( ctx.endsWith( 'image' ) && attributeName == 'imageStyle' ) {
+					return false;
+				}
+			} );
+
+			editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
+
+			expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should convert model to view: adding attribute', () => {
+			setModelData( model, '<image src="foo.png"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', 'sideStyle', image );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image side-class" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should convert model to view: removing attribute', () => {
+			setModelData( model, '<image src="foo.png" imageStyle="sideStyle"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', null, image );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should convert model to view: change attribute', () => {
+			setModelData( model, '<image src="foo.png" imageStyle="dummy"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', 'sideStyle', image );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
+
+			// https://github.com/ckeditor/ckeditor5-image/issues/132
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image side-class" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', 'dummyStyle', image );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image dummy-class"><img src="foo.png"></figure>' );
+
+			// https://github.com/ckeditor/ckeditor5-image/issues/132
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget dummy-class image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should not convert from model to view if already consumed: adding attribute', () => {
+			editor.editing.downcastDispatcher.on( 'attribute:imageStyle', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'attribute:imageStyle' );
+			}, { priority: 'high' } );
+
+			setModelData( model, '<image src="foo.png"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', 'sideStyle', image );
+			} );
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should not set attribute if change was already consumed', () => {
+			editor.editing.downcastDispatcher.on( 'attribute:imageStyle', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'attribute:imageStyle' );
+			}, { priority: 'high' } );
+
+			setModelData( model, '<image src="foo.png" imageStyle="dummyStyle"></image>' );
+
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should not convert from model to view if style is not present: adding attribute', () => {
+			setModelData( model, '<image src="foo.png"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', 'foo', image );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should not convert from model to view if style is not present: change attribute', () => {
+			setModelData( model, '<image src="foo.png" imageStyle="dummy"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', 'foo', image );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+
+		it( 'should not convert from model to view if style is not present: remove attribute', () => {
+			setModelData( model, '<image src="foo.png" imageStyle="foo"></image>' );
+			const image = document.getRoot().getChild( 0 );
+
+			model.change( writer => {
+				writer.setAttribute( 'imageStyle', null, image );
+			} );
+
+			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
+			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
+				'<figure class="ck-widget image" contenteditable="false"><img src="foo.png"></img></figure>'
+			);
+		} );
+	} );
+
+	describe( 'config', () => {
+		it( 'should fall back to defaults when no image.styles', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEditing ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'full', 'side' ] );
+				} );
+		} );
+
+		it( 'should not alter the image.styles config', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEditing ],
+					image: {
+						styles: [
+							'side'
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'side' ] );
+				} );
+		} );
+
+		it( 'should not alter object definitions in the image.styles config', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleEditing ],
+					image: {
+						styles: [
+							{ name: 'side' }
+						]
+					}
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ { name: 'side' } ] );
+				} );
+		} );
+	} );
+} );

+ 0 - 515
packages/ckeditor5-image/tests/imagestyle/imagestyleengine.js

@@ -1,515 +0,0 @@
-/**
- * @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 ImageStyleEngine from '../../src/imagestyle/imagestyleengine';
-import ImageEngine from '../../src/image/imageengine';
-import ImageStyleCommand from '../../src/imagestyle/imagestylecommand';
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-
-import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
-import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
-import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
-import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
-
-describe( 'ImageStyleEngine', () => {
-	let editor, plugin, document, viewDocument;
-
-	afterEach( () => {
-		editor.destroy();
-	} );
-
-	describe( 'plugin', () => {
-		beforeEach( () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ],
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-				} );
-		} );
-
-		it( 'should be loaded', () => {
-			expect( editor.plugins.get( ImageStyleEngine ) ).to.be.instanceOf( ImageStyleEngine );
-		} );
-
-		it( 'should load image engine', () => {
-			expect( editor.plugins.get( ImageEngine ) ).to.be.instanceOf( ImageEngine );
-		} );
-	} );
-
-	describe( 'init', () => {
-		beforeEach( () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ],
-					image: {
-						styles: [
-							{ name: 'fullStyle', title: 'foo', icon: 'object-center', isDefault: true },
-							{ name: 'sideStyle', title: 'bar', icon: 'object-right', className: 'side-class' },
-							{ name: 'dummyStyle', title: 'baz', icon: 'object-dummy', className: 'dummy-class' },
-						]
-					}
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-					document = editor.document;
-					viewDocument = editor.editing.view;
-				} );
-		} );
-
-		it( 'should define image.styles config', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ]
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-
-					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'imageStyleFull', 'imageStyleSide' ] );
-				} );
-		} );
-
-		it( 'should set schema rules for image style', () => {
-			const schema = document.schema;
-
-			expect( schema.check( { name: 'image', attributes: [ 'imageStyle', 'src' ], inside: '$root' } ) ).to.be.true;
-		} );
-
-		it( 'should register separate command for each style', () => {
-			expect( editor.commands.get( 'fullStyle' ) ).to.be.instanceOf( ImageStyleCommand );
-			expect( editor.commands.get( 'sideStyle' ) ).to.be.instanceOf( ImageStyleCommand );
-			expect( editor.commands.get( 'dummyStyle' ) ).to.be.instanceOf( ImageStyleCommand );
-		} );
-
-		it( 'should convert from view to model', () => {
-			editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
-
-			expect( getModelData( document, { withoutSelection: true } ) )
-				.to.equal( '<image imageStyle="sideStyle" src="foo.png"></image>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget side-class" contenteditable="false">' +
-					'<img src="foo.png"></img>' +
-				'</figure>' );
-		} );
-
-		it( 'should not convert from view to model if class is not defined', () => {
-			editor.setData( '<figure class="image foo-bar"><img src="foo.png" /></figure>' );
-
-			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should not convert from view to model when not in image figure', () => {
-			editor.setData( '<figure class="side-class"></figure>' );
-
-			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal( '' );
-		} );
-
-		it( 'should not convert from view to model if schema prevents it', () => {
-			document.schema.disallow( { name: 'image', attributes: 'imageStyle' } );
-			editor.setData( '<figure class="image side-class"><img src="foo.png" /></figure>' );
-
-			expect( getModelData( document, { withoutSelection: true } ) ).to.equal( '<image src="foo.png"></image>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should convert model to view: adding attribute', () => {
-			setModelData( document, '<image src="foo.png"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
-			} );
-
-			expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should convert model to view: removing attribute', () => {
-			setModelData( document, '<image src="foo.png" imageStyle="sideStyle"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', null );
-			} );
-
-			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should convert model to view: change attribute', () => {
-			setModelData( document, '<image src="foo.png" imageStyle="dummy"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
-			} );
-
-			expect( editor.getData() ).to.equal( '<figure class="image side-class"><img src="foo.png"></figure>' );
-
-			// https://github.com/ckeditor/ckeditor5-image/issues/132
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', 'dummyStyle' );
-			} );
-
-			expect( editor.getData() ).to.equal( '<figure class="image dummy-class"><img src="foo.png"></figure>' );
-
-			// https://github.com/ckeditor/ckeditor5-image/issues/132
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget dummy-class" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should not convert from model to view if already consumed: adding attribute', () => {
-			editor.editing.modelToView.on( 'addAttribute:imageStyle', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'addAttribute:imageStyle' );
-			}, { priority: 'high' } );
-
-			setModelData( document, '<image src="foo.png"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
-			} );
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should not convert from model to view if already consumed: removing attribute', () => {
-			editor.editing.modelToView.on( 'removeAttribute:imageStyle', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'removeAttribute:imageStyle' );
-			}, { priority: 'high' } );
-
-			setModelData( document, '<image src="foo.png" imageStyle="sideStyle"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', null );
-			} );
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget side-class" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should not convert from model to view if already consumed: change attribute', () => {
-			editor.editing.modelToView.on( 'changeAttribute:imageStyle', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'changeAttribute:imageStyle' );
-			}, { priority: 'high' } );
-
-			setModelData( document, '<image src="foo.png" imageStyle="dummyStyle"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', 'sideStyle' );
-			} );
-
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget dummy-class" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should not convert from model to view if style is not present: adding attribute', () => {
-			setModelData( document, '<image src="foo.png"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', 'foo' );
-			} );
-
-			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should not convert from model to view if style is not present: change attribute', () => {
-			setModelData( document, '<image src="foo.png" imageStyle="dummy"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', 'foo' );
-			} );
-
-			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-
-		it( 'should not convert from model to view if style is not present: remove attribute', () => {
-			setModelData( document, '<image src="foo.png" imageStyle="foo"></image>' );
-			const image = document.getRoot().getChild( 0 );
-			const batch = document.batch();
-
-			document.enqueueChanges( () => {
-				batch.setAttribute( image, 'imageStyle', null );
-			} );
-
-			expect( editor.getData() ).to.equal( '<figure class="image"><img src="foo.png"></figure>' );
-			expect( getViewData( viewDocument, { withoutSelection: true } ) ).to.equal(
-				'<figure class="image ck-widget" contenteditable="false"><img src="foo.png"></img></figure>'
-			);
-		} );
-	} );
-
-	describe( 'imageStyles()', () => {
-		it( 'should fall back to defaults when no image.styles', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ]
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-
-					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'imageStyleFull', 'imageStyleSide' ] );
-				} );
-		} );
-
-		it( 'should not alter the image.styles config', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ],
-					image: {
-						styles: [
-							'imageStyleSide'
-						]
-					}
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-
-					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ 'imageStyleSide' ] );
-				} );
-		} );
-
-		it( 'should not alter object definitions in the image.styles config', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ],
-					image: {
-						styles: [
-							{ name: 'imageStyleSide' }
-						]
-					}
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-
-					expect( newEditor.config.get( 'image.styles' ) ).to.deep.equal( [ { name: 'imageStyleSide' } ] );
-				} );
-		} );
-
-		it( 'should cache the styles', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ]
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-					plugin = editor.plugins.get( ImageStyleEngine );
-
-					expect( plugin.imageStyles ).to.equal( plugin.imageStyles );
-				} );
-		} );
-
-		describe( 'object format', () => {
-			beforeEach( () => {
-				class TranslationMock extends Plugin {
-					init() {
-						sinon.stub( this.editor, 't' ).returns( 'Default translation' );
-					}
-				}
-
-				return VirtualTestEditor
-					.create( {
-						plugins: [ TranslationMock, ImageStyleEngine ],
-						image: {
-							styles: [
-								// Custom user styles.
-								{ name: 'foo', title: 'foo', icon: 'custom', isDefault: true, className: 'foo-class' },
-								{ name: 'bar', title: 'bar', icon: 'right', className: 'bar-class' },
-								{ name: 'baz', title: 'Side image', icon: 'custom', className: 'baz-class' },
-
-								// Customized default styles.
-								{ name: 'imageStyleFull', icon: 'left', title: 'Custom title' }
-							]
-						}
-					} )
-					.then( newEditor => {
-						editor = newEditor;
-						plugin = editor.plugins.get( ImageStyleEngine );
-					} );
-			} );
-
-			it( 'should pass through if #name not found in default styles', () => {
-				expect( plugin.imageStyles[ 0 ] ).to.deep.equal( {
-					name: 'foo',
-					title: 'foo',
-					icon: 'custom',
-					isDefault: true,
-					className: 'foo-class'
-				} );
-			} );
-
-			it( 'should use one of default icons if #icon matches', () => {
-				expect( plugin.imageStyles[ 1 ].icon ).to.equal( ImageStyleEngine.defaultIcons.right );
-			} );
-
-			it( 'should use one of default translations if #title matches', () => {
-				expect( plugin.imageStyles[ 2 ].title ).to.deep.equal( 'Default translation' );
-			} );
-
-			it( 'should extend one of default styles if #name matches', () => {
-				expect( plugin.imageStyles[ 3 ] ).to.deep.equal( {
-					name: 'imageStyleFull',
-					title: 'Custom title',
-					icon: ImageStyleEngine.defaultIcons.left,
-					isDefault: true
-				} );
-			} );
-		} );
-
-		describe( 'string format', () => {
-			it( 'should use one of default styles if #name matches', () => {
-				return VirtualTestEditor
-					.create( {
-						plugins: [ ImageStyleEngine ],
-						image: {
-							styles: [ 'imageStyleFull' ]
-						}
-					} )
-					.then( newEditor => {
-						editor = newEditor;
-						plugin = editor.plugins.get( ImageStyleEngine );
-						expect( plugin.imageStyles[ 0 ] ).to.deep.equal( ImageStyleEngine.defaultStyles.imageStyleFull );
-					} );
-			} );
-
-			it( 'should warn if a #name not found in default styles', () => {
-				sinon.stub( log, 'warn' );
-
-				return VirtualTestEditor
-					.create( {
-						plugins: [ ImageStyleEngine ],
-						image: {
-							styles: [ 'foo' ]
-						}
-					} )
-					.then( newEditor => {
-						editor = newEditor;
-						plugin = editor.plugins.get( ImageStyleEngine );
-
-						expect( plugin.imageStyles[ 0 ] ).to.deep.equal( {
-							name: 'foo'
-						} );
-
-						sinon.assert.calledOnce( log.warn );
-						sinon.assert.calledWithExactly( log.warn,
-							sinon.match( /^image-style-not-found/ ),
-							{ name: 'foo' }
-						);
-					} );
-			} );
-		} );
-	} );
-
-	describe( 'localizedDefaultStylesTitles()', () => {
-		it( 'should return localized titles of default styles', () => {
-			return VirtualTestEditor
-				.create( {
-					plugins: [ ImageStyleEngine ]
-				} )
-				.then( newEditor => {
-					editor = newEditor;
-					plugin = editor.plugins.get( ImageStyleEngine );
-
-					expect( plugin.localizedDefaultStylesTitles ).to.deep.equal( {
-						'Full size image': 'Full size image',
-						'Side image': 'Side image',
-						'Left aligned image': 'Left aligned image',
-						'Centered image': 'Centered image',
-						'Right aligned image': 'Right aligned image'
-					} );
-				} );
-		} );
-	} );
-
-	describe( 'defaultStyles', () => {
-		it( 'should be defined', () => {
-			expect( ImageStyleEngine.defaultStyles ).to.deep.equal( {
-				imageStyleFull: {
-					name: 'imageStyleFull',
-					title: 'Full size image',
-					icon: fullWidthIcon,
-					isDefault: true
-				},
-				imageStyleSide: {
-					name: 'imageStyleSide',
-					title: 'Side image',
-					icon: rightIcon,
-					className: 'image-style-side'
-				},
-				imageStyleAlignLeft: {
-					name: 'imageStyleAlignLeft',
-					title: 'Left aligned image',
-					icon: leftIcon,
-					className: 'image-style-align-left'
-				},
-				imageStyleAlignCenter: {
-					name: 'imageStyleAlignCenter',
-					title: 'Centered image',
-					icon: centerIcon,
-					className: 'image-style-align-center'
-				},
-				imageStyleAlignRight: {
-					name: 'imageStyleAlignRight',
-					title: 'Right aligned image',
-					icon: rightIcon,
-					className: 'image-style-align-right'
-				}
-			} );
-		} );
-	} );
-
-	describe( 'defaultIcons', () => {
-		it( 'should be defined', () => {
-			expect( ImageStyleEngine.defaultIcons ).to.deep.equal( {
-				full: fullWidthIcon,
-				left: leftIcon,
-				right: rightIcon,
-				center: centerIcon,
-			} );
-		} );
-	} );
-} );

+ 135 - 0
packages/ckeditor5-image/tests/imagestyle/imagestyleui.js

@@ -0,0 +1,135 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import ImageStyleEditing from '../../src/imagestyle/imagestyleediting';
+import ImageStyleUI from '../../src/imagestyle/imagestyleui';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+
+describe( 'ImageStyleUI', () => {
+	let editor;
+
+	const styles = [
+		{ name: 'style 1', title: 'Style 1 title', icon: 'style1-icon', isDefault: true },
+		{ name: 'style 2', title: 'Style 2 title', icon: 'style2-icon', cssClass: 'style2-class' },
+		{ name: 'style 3', title: 'Style 3 title', icon: 'style3-icon', cssClass: 'style3-class' }
+	];
+
+	beforeEach( () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ ImageStyleEditing, ImageStyleUI ],
+				image: {
+					styles
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	it( 'should register buttons for each style', () => {
+		const spy = sinon.spy( editor, 'execute' );
+
+		const command = editor.commands.get( 'imageStyle' );
+
+		for ( const style of styles ) {
+			const buttonView = editor.ui.componentFactory.create( `imageStyle:${ style.name }` );
+
+			expect( buttonView ).to.be.instanceOf( ButtonView );
+			expect( buttonView.label ).to.equal( style.title );
+			expect( buttonView.icon ).to.equal( style.icon );
+
+			command.isEnabled = true;
+			expect( buttonView.isEnabled ).to.be.true;
+			command.isEnabled = false;
+			expect( buttonView.isEnabled ).to.be.false;
+
+			buttonView.fire( 'execute' );
+			sinon.assert.calledWithExactly( editor.execute, 'imageStyle', { value: style.name } );
+
+			spy.reset();
+		}
+	} );
+
+	it( 'should translate buttons if taken from default styles', () => {
+		const editorElement = global.document.createElement( 'div' );
+
+		global.document.body.appendChild( editorElement );
+
+		class TranslationMock extends Plugin {
+			init() {
+				sinon.stub( this.editor, 't' ).returns( 'Default title' );
+			}
+		}
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ TranslationMock, ImageStyleEditing, ImageStyleUI ],
+				image: {
+					styles: [
+						{ name: 'style 1', title: 'Side image', icon: 'style1-icon', isDefault: true }
+					]
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+
+				const buttonView = editor.ui.componentFactory.create( 'imageStyle:style 1' );
+
+				expect( buttonView.label ).to.equal( 'Default title' );
+			} );
+	} );
+
+	it( 'should not add buttons to image toolbar if configuration is present', () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ ImageStyleEditing, ImageStyleUI ],
+				image: {
+					styles,
+					toolbar: [ 'foo', 'bar' ]
+				}
+			} )
+			.then( newEditor => {
+				expect( newEditor.config.get( 'image.toolbar' ) ).to.deep.equal( [ 'foo', 'bar' ] );
+				newEditor.destroy();
+			} );
+	} );
+
+	describe( 'localizedDefaultStylesTitles()', () => {
+		it( 'should return localized titles of default styles', () => {
+			return VirtualTestEditor
+				.create( {
+					plugins: [ ImageStyleUI ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					const plugin = editor.plugins.get( ImageStyleUI );
+
+					expect( plugin.localizedDefaultStylesTitles ).to.deep.equal( {
+						'Full size image': 'Full size image',
+						'Side image': 'Side image',
+						'Left aligned image': 'Left aligned image',
+						'Centered image': 'Centered image',
+						'Right aligned image': 'Right aligned image'
+					} );
+				} );
+		} );
+	} );
+} );

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

@@ -0,0 +1,117 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+import fullWidthIcon from '@ckeditor/ckeditor5-core/theme/icons/object-full-width.svg';
+import leftIcon from '@ckeditor/ckeditor5-core/theme/icons/object-left.svg';
+import centerIcon from '@ckeditor/ckeditor5-core/theme/icons/object-center.svg';
+import rightIcon from '@ckeditor/ckeditor5-core/theme/icons/object-right.svg';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { normalizeImageStyles } from '../../src/imagestyle/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'ImageStyle utils', () => {
+	let imageStyles;
+
+	describe( 'normalizeImageStyles()', () => {
+		// Since this function is all about normalizing the config object, make sure it doesn't throw
+		// if the config is empty (which may happen e.g. if only ImageStyleUI was loaded).
+		it( 'does not throw when given undefined', () => {
+			expect( normalizeImageStyles() ).to.deep.equal( [] );
+		} );
+
+		describe( 'object format', () => {
+			beforeEach( () => {
+				imageStyles = normalizeImageStyles( [
+					{ name: 'foo', title: 'foo', icon: 'custom', isDefault: true, className: 'foo-class' },
+					{ name: 'bar', title: 'bar', icon: 'right', className: 'bar-class' },
+					{ name: 'baz', title: 'Side image', icon: 'custom', className: 'baz-class' },
+
+					// Customized default styles.
+					{ name: 'full', icon: 'left', title: 'Custom title' }
+				] );
+			} );
+
+			it( 'should pass through if #name not found in default styles', () => {
+				expect( imageStyles[ 0 ] ).to.deep.equal( {
+					name: 'foo',
+					title: 'foo',
+					icon: 'custom',
+					isDefault: true,
+					className: 'foo-class'
+				} );
+			} );
+
+			it( 'should use one of default icons if #icon matches', () => {
+				expect( imageStyles[ 1 ].icon ).to.equal( rightIcon );
+			} );
+
+			it( 'should extend one of default styles if #name matches', () => {
+				expect( imageStyles[ 3 ] ).to.deep.equal( {
+					name: 'full',
+					title: 'Custom title',
+					icon: leftIcon,
+					isDefault: true
+				} );
+			} );
+		} );
+
+		describe( 'string format', () => {
+			it( 'should use one of default styles if #name matches', () => {
+				expect( normalizeImageStyles( [ 'full' ] ) ).to.deep.equal( [ {
+					name: 'full',
+					title: 'Full size image',
+					icon: fullWidthIcon,
+					isDefault: true
+				} ] );
+
+				expect( normalizeImageStyles( [ 'side' ] ) ).to.deep.equal( [ {
+					name: 'side',
+					title: 'Side image',
+					icon: rightIcon,
+					className: 'image-style-side'
+				} ] );
+
+				expect( normalizeImageStyles( [ 'alignLeft' ] ) ).to.deep.equal( [ {
+					name: 'alignLeft',
+					title: 'Left aligned image',
+					icon: leftIcon,
+					className: 'image-style-align-left'
+				} ] );
+
+				expect( normalizeImageStyles( [ 'alignCenter' ] ) ).to.deep.equal( [ {
+					name: 'alignCenter',
+					title: 'Centered image',
+					icon: centerIcon,
+					className: 'image-style-align-center'
+				} ] );
+
+				expect( normalizeImageStyles( [ 'alignRight' ] ) ).to.deep.equal( [ {
+					name: 'alignRight',
+					title: 'Right aligned image',
+					icon: rightIcon,
+					className: 'image-style-align-right'
+				} ] );
+			} );
+
+			it( 'should warn if a #name not found in default styles', () => {
+				testUtils.sinon.stub( log, 'warn' );
+
+				expect( normalizeImageStyles( [ 'foo' ] ) ).to.deep.equal( [ {
+					name: 'foo'
+				} ] );
+
+				sinon.assert.calledOnce( log.warn );
+				sinon.assert.calledWithExactly( log.warn,
+					sinon.match( /^image-style-not-found/ ),
+					{ name: 'foo' }
+				);
+			} );
+		} );
+	} );
+} );

+ 8 - 196
packages/ckeditor5-image/tests/imagetextalternative.js

@@ -1,22 +1,17 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
-/* global Event, document */
-
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Image from '../src/image';
 import ImageTextAlternative from '../src/imagetextalternative';
-import ImageTextAlternativeEngine from '../src/imagetextalternative/imagetextalternativeengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import View from '@ckeditor/ckeditor5-ui/src/view';
+import ImageTextAlternativeEditing from '../src/imagetextalternative/imagetextalternativeediting';
+import ImageTextAlternativeUI from '../src/imagetextalternative/imagetextalternativeui';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 describe( 'ImageTextAlternative', () => {
-	let editor, editingView, doc, plugin, command, form, balloon, editorElement, button;
+	let editor, plugin, editorElement;
 
 	beforeEach( () => {
 		editorElement = global.document.createElement( 'div' );
@@ -28,14 +23,8 @@ describe( 'ImageTextAlternative', () => {
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				editingView = editor.editing.view;
-				doc = editor.document;
 				newEditor.editing.view.attachDomRoot( editorElement );
 				plugin = editor.plugins.get( ImageTextAlternative );
-				command = editor.commands.get( 'imageTextAlternative' );
-				form = plugin._form;
-				balloon = editor.plugins.get( 'ContextualBalloon' );
-				button = editor.ui.componentFactory.create( 'imageTextAlternative' );
 			} );
 	} );
 
@@ -49,188 +38,11 @@ describe( 'ImageTextAlternative', () => {
 		expect( plugin ).to.be.instanceOf( ImageTextAlternative );
 	} );
 
-	it( 'should load ImageTextAlternativeEngine plugin', () => {
-		expect( editor.plugins.get( ImageTextAlternativeEngine ) ).to.be.instanceOf( ImageTextAlternativeEngine );
-	} );
-
-	describe( 'toolbar button', () => {
-		it( 'should be registered in component factory', () => {
-			expect( button ).to.be.instanceOf( ButtonView );
-		} );
-
-		it( 'should have isEnabled property bind to command\'s isEnabled property', () => {
-			command.isEnabled = true;
-			expect( button.isEnabled ).to.be.true;
-
-			command.isEnabled = false;
-			expect( button.isEnabled ).to.be.false;
-		} );
-
-		it( 'should show balloon panel on execute', () => {
-			expect( balloon.visibleView ).to.be.null;
-
-			setData( doc, '[<image src="" alt="foo bar"></image>]' );
-
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-
-			// Make sure successive execute does not throw, e.g. attempting
-			// to display the form twice.
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-		} );
-
-		it( 'should set alt attribute value to textarea and select it', () => {
-			const spy = sinon.spy( form.labeledInput, 'select' );
-
-			setData( doc, '[<image src="" alt="foo bar"></image>]' );
-
-			button.fire( 'execute' );
-			sinon.assert.calledOnce( spy );
-			expect( form.labeledInput.value ).equals( 'foo bar' );
-		} );
-
-		it( 'should set empty text to textarea and select it when there is no alt attribute', () => {
-			const spy = sinon.spy( form.labeledInput, 'select' );
-
-			setData( doc, '[<image src=""></image>]' );
-
-			button.fire( 'execute' );
-			sinon.assert.calledOnce( spy );
-			expect( form.labeledInput.value ).equals( '' );
-		} );
+	it( 'should load ImageTextAlternativeEditing plugin', () => {
+		expect( editor.plugins.get( ImageTextAlternativeEditing ) ).to.be.instanceOf( ImageTextAlternativeEditing );
 	} );
 
-	describe( 'balloon panel form', () => {
-		// https://github.com/ckeditor/ckeditor5-image/issues/114
-		it( 'should make sure the input always stays in sync with the value of the command', () => {
-			const button = editor.ui.componentFactory.create( 'imageTextAlternative' );
-
-			// Mock the value of the input after some past editing.
-			form.labeledInput.value = 'foo';
-
-			// Mock the user using the form, changing the value but clicking "Cancel".
-			// so the command's value is not updated.
-			form.labeledInput.inputView.element.value = 'This value was canceled.';
-
-			// Mock the user editing the same image once again.
-			setData( doc, '[<image src="" alt="foo"></image>]' );
-
-			button.fire( 'execute' );
-			expect( form.labeledInput.inputView.element.value ).to.equal( 'foo' );
-		} );
-
-		it( 'should execute command on submit', () => {
-			const spy = sinon.spy( editor, 'execute' );
-			form.fire( 'submit' );
-
-			sinon.assert.calledOnce( spy );
-			sinon.assert.calledWithExactly( spy, 'imageTextAlternative', {
-				newValue: form.labeledInput.inputView.element.value
-			} );
-		} );
-
-		it( 'should hide the panel on cancel and focus the editing view', () => {
-			const spy = sinon.spy( editor.editing.view, 'focus' );
-
-			setData( doc, '[<image src="" alt="foo bar"></image>]' );
-
-			editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-
-			form.fire( 'cancel' );
-			expect( balloon.visibleView ).to.be.null;
-			sinon.assert.calledOnce( spy );
-		} );
-
-		it( 'should not engage when the form is in the balloon yet invisible', () => {
-			setData( doc, '[<image src=""></image>]' );
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( form );
-
-			const lastView = new View();
-			lastView.element = document.createElement( 'div' );
-
-			balloon.add( { view: lastView } );
-			expect( balloon.visibleView ).to.equal( lastView );
-
-			button.fire( 'execute' );
-			expect( balloon.visibleView ).to.equal( lastView );
-		} );
-
-		describe( 'integration with the editor selection (#render event)', () => {
-			it( 'should re-position the form', () => {
-				setData( doc, '[<image src=""></image>]' );
-				button.fire( 'execute' );
-
-				const spy = sinon.spy( balloon, 'updatePosition' );
-
-				editingView.fire( 'render' );
-				sinon.assert.calledOnce( spy );
-			} );
-
-			it( 'should hide the form and focus editable when image widget has been removed by external change', () => {
-				setData( doc, '[<image src=""></image>]' );
-				button.fire( 'execute' );
-
-				const remveSpy = sinon.spy( balloon, 'remove' );
-				const focusSpy = sinon.spy( editor.editing.view, 'focus' );
-
-				// EnqueueChanges automatically fires #render event.
-				doc.enqueueChanges( () => {
-					doc.batch( 'transparent' ).remove( doc.selection.getFirstRange() );
-				} );
-
-				sinon.assert.calledWithExactly( remveSpy, form );
-				sinon.assert.calledOnce( focusSpy );
-			} );
-		} );
-
-		describe( 'close listeners', () => {
-			describe( 'keyboard', () => {
-				it( 'should close upon Esc key press and focus the editing view', () => {
-					const hideSpy = sinon.spy( plugin, '_hideForm' );
-					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
-
-					setData( doc, '[<image src=""></image>]' );
-					button.fire( 'execute' );
-
-					const keyEvtData = {
-						keyCode: keyCodes.esc,
-						preventDefault: sinon.spy(),
-						stopPropagation: sinon.spy()
-					};
-
-					form.keystrokes.press( keyEvtData );
-					sinon.assert.calledOnce( hideSpy );
-					sinon.assert.calledOnce( keyEvtData.preventDefault );
-					sinon.assert.calledOnce( focusSpy );
-				} );
-			} );
-
-			describe( 'mouse', () => {
-				it( 'should close and not focus editable on click outside the panel', () => {
-					const hideSpy = sinon.spy( plugin, '_hideForm' );
-					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
-
-					setData( doc, '[<image src=""></image>]' );
-					button.fire( 'execute' );
-
-					global.document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
-					sinon.assert.called( hideSpy );
-					sinon.assert.notCalled( focusSpy );
-				} );
-
-				it( 'should not close on click inside the panel', () => {
-					const spy = sinon.spy( plugin, '_hideForm' );
-
-					setData( doc, '[<image src=""></image>]' );
-					button.fire( 'execute' );
-
-					form.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
-					sinon.assert.notCalled( spy );
-				} );
-			} );
-		} );
+	it( 'should load ImageTextAlternativeUI plugin', () => {
+		expect( editor.plugins.get( ImageTextAlternativeUI ) ).to.be.instanceOf( ImageTextAlternativeUI );
 	} );
 } );

+ 26 - 24
packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativecommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,78 +8,80 @@ import ImageTextAlternativeCommand from '../../src/imagetextalternative/imagetex
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'ImageTextAlternativeCommand', () => {
-	let document, command;
+	let model, command;
 
 	beforeEach( () => {
 		return ModelTestEditor.create()
 			.then( newEditor => {
-				document = newEditor.document;
+				model = newEditor.model;
 				command = new ImageTextAlternativeCommand( newEditor );
 
-				document.schema.registerItem( 'p', '$block' );
+				model.schema.register( 'p', { inheritAllFrom: '$block' } );
 
-				document.schema.registerItem( 'image' );
-				document.schema.requireAttributes( 'image', [ 'src' ] );
-				document.schema.allow( { name: 'image', attributes: [ 'alt', 'src' ], inside: '$root' } );
-				document.schema.objects.add( 'image' );
+				model.schema.register( 'image', {
+					allowWhere: '$block',
+					isObject: true,
+					isBlock: true,
+					alllowAttributes: [ 'alt', 'src' ]
+				} );
 			} );
 	} );
 
 	it( 'should have false value if no image is selected', () => {
-		setData( document, '[]<p></p>' );
+		setData( model, '[]<p></p>' );
 
 		expect( command.value ).to.be.false;
 	} );
 
 	it( 'should have false value if image without alt is selected', () => {
-		setData( document, '[<image src="image.png"></image>]' );
+		setData( model, '[<image src="image.png"></image>]' );
 
 		expect( command.value ).to.be.false;
 	} );
 
 	it( 'should be disabled if not on image element', () => {
-		setData( document, '[]<p></p>' );
+		setData( model, '[]<p></p>' );
 
 		expect( command.isEnabled ).to.be.false;
 	} );
 
 	it( 'should be enabled on image element without alt attribute', () => {
-		setData( document, '[<image src="image.png"></image>]' );
+		setData( model, '[<image src="image.png"></image>]' );
 
 		expect( command.isEnabled ).to.be.true;
 	} );
 
 	it( 'should have proper value if on image element with alt attribute', () => {
-		setData( document, '[<image src="image.png" alt="foo bar baz"></image>]' );
+		setData( model, '[<image src="image.png" alt="foo bar baz"></image>]' );
 
 		expect( command.value ).to.equal( 'foo bar baz' );
 	} );
 
 	it( 'should set proper alt if executed on image without alt attribute', () => {
-		setData( document, '[<image src="image.png"></image>]' );
+		setData( model, '[<image src="image.png"></image>]' );
 
 		command.execute( { newValue: 'fiz buz' } );
 
-		expect( getData( document ) ).to.equal( '[<image alt="fiz buz" src="image.png"></image>]' );
+		expect( getData( model ) ).to.equal( '[<image alt="fiz buz" src="image.png"></image>]' );
 	} );
 
 	it( 'should change alt if executed on image with alt attribute', () => {
-		setData( document, '[<image alt="foo bar" src="image.png"></image>]' );
+		setData( model, '[<image alt="foo bar" src="image.png"></image>]' );
 
 		command.execute( { newValue: 'fiz buz' } );
 
-		expect( getData( document ) ).to.equal( '[<image alt="fiz buz" src="image.png"></image>]' );
+		expect( getData( model ) ).to.equal( '[<image alt="fiz buz" src="image.png"></image>]' );
 	} );
 
-	it( 'should allow to provide batch instance', () => {
-		const batch = document.batch();
-		const spy = sinon.spy( batch, 'setAttribute' );
+	it( 'should use parent batch', () => {
+		setData( model, '[<image src="image.png"></image>]' );
 
-		setData( document, '[<image src="image.png"></image>]' );
+		model.change( writer => {
+			expect( writer.batch.deltas ).to.length( 0 );
 
-		command.execute( { newValue: 'foo bar', batch } );
+			command.execute( { newValue: 'foo bar' } );
 
-		expect( getData( document ) ).to.equal( '[<image alt="foo bar" src="image.png"></image>]' );
-		sinon.assert.calledOnce( spy );
+			expect( writer.batch.deltas ).to.length.above( 0 );
+		} );
 	} );
 } );

+ 6 - 5
packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeengine.js → packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeediting.js

@@ -1,25 +1,26 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import ImageTextAlternativeEngine from '../../src/imagetextalternative/imagetextalternativeengine';
+import ImageTextAlternativeEditing from '../../src/imagetextalternative/imagetextalternativeediting';
 import ImageTextAlternativeCommand from '../../src/imagetextalternative/imagetextalternativecommand';
 
-describe( 'ImageTextAlternativeEngine', () => {
+describe( 'ImageTextAlternativeEditing', () => {
 	let editor;
+
 	beforeEach( () => {
 		return VirtualTestEditor
 			.create( {
-				plugins: [ ImageTextAlternativeEngine ]
+				plugins: [ ImageTextAlternativeEditing ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
 			} );
 	} );
 
-	it( 'should register ImageAlteranteTextCommand', () => {
+	it( 'should register ImageAlternativeTextCommand', () => {
 		expect( editor.commands.get( 'imageTextAlternative' ) ).to.be.instanceOf( ImageTextAlternativeCommand );
 	} );
 } );

+ 228 - 0
packages/ckeditor5-image/tests/imagetextalternative/imagetextalternativeui.js

@@ -0,0 +1,228 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global Event, document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Image from '../../src/image';
+import ImageTextAlternativeEditing from '../../src/imagetextalternative/imagetextalternativeediting';
+import ImageTextAlternativeUI from '../../src/imagetextalternative/imagetextalternativeui';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+
+describe( 'ImageTextAlternative', () => {
+	let editor, model, doc, plugin, command, form, balloon, editorElement, button;
+
+	beforeEach( () => {
+		editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ ImageTextAlternativeEditing, ImageTextAlternativeUI, Image ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				doc = model.document;
+				newEditor.editing.view.attachDomRoot( editorElement );
+				plugin = editor.plugins.get( ImageTextAlternativeUI );
+				command = editor.commands.get( 'imageTextAlternative' );
+				form = plugin._form;
+				balloon = editor.plugins.get( 'ContextualBalloon' );
+				button = editor.ui.componentFactory.create( 'imageTextAlternative' );
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	describe( 'toolbar button', () => {
+		it( 'should be registered in component factory', () => {
+			expect( button ).to.be.instanceOf( ButtonView );
+		} );
+
+		it( 'should have isEnabled property bind to command\'s isEnabled property', () => {
+			command.isEnabled = true;
+			expect( button.isEnabled ).to.be.true;
+
+			command.isEnabled = false;
+			expect( button.isEnabled ).to.be.false;
+		} );
+
+		it( 'should show balloon panel on execute', () => {
+			expect( balloon.visibleView ).to.be.null;
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+
+			// Make sure successive execute does not throw, e.g. attempting
+			// to display the form twice.
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+		} );
+
+		it( 'should set alt attribute value to textarea and select it', () => {
+			const spy = sinon.spy( form.labeledInput, 'select' );
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			button.fire( 'execute' );
+			sinon.assert.calledOnce( spy );
+			expect( form.labeledInput.value ).equals( 'foo bar' );
+		} );
+
+		it( 'should set empty text to textarea and select it when there is no alt attribute', () => {
+			const spy = sinon.spy( form.labeledInput, 'select' );
+
+			setData( model, '[<image src=""></image>]' );
+
+			button.fire( 'execute' );
+			sinon.assert.calledOnce( spy );
+			expect( form.labeledInput.value ).equals( '' );
+		} );
+	} );
+
+	describe( 'balloon panel form', () => {
+		// https://github.com/ckeditor/ckeditor5-image/issues/114
+		it( 'should make sure the input always stays in sync with the value of the command', () => {
+			const button = editor.ui.componentFactory.create( 'imageTextAlternative' );
+
+			// Mock the value of the input after some past editing.
+			form.labeledInput.value = 'foo';
+
+			// Mock the user using the form, changing the value but clicking "Cancel".
+			// so the command's value is not updated.
+			form.labeledInput.inputView.element.value = 'This value was canceled.';
+
+			// Mock the user editing the same image once again.
+			setData( model, '[<image src="" alt="foo"></image>]' );
+
+			button.fire( 'execute' );
+			expect( form.labeledInput.inputView.element.value ).to.equal( 'foo' );
+		} );
+
+		it( 'should execute command on submit', () => {
+			const spy = sinon.spy( editor, 'execute' );
+			form.fire( 'submit' );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, 'imageTextAlternative', {
+				newValue: form.labeledInput.inputView.element.value
+			} );
+		} );
+
+		it( 'should hide the panel on cancel and focus the editing view', () => {
+			const spy = sinon.spy( editor.editing.view, 'focus' );
+
+			setData( model, '[<image src="" alt="foo bar"></image>]' );
+
+			editor.ui.componentFactory.create( 'imageTextAlternative' ).fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+
+			form.fire( 'cancel' );
+			expect( balloon.visibleView ).to.be.null;
+			sinon.assert.calledOnce( spy );
+		} );
+
+		it( 'should not engage when the form is in the balloon yet invisible', () => {
+			setData( model, '[<image src=""></image>]' );
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( form );
+
+			const lastView = new View();
+			lastView.element = document.createElement( 'div' );
+
+			balloon.add( { view: lastView } );
+			expect( balloon.visibleView ).to.equal( lastView );
+
+			button.fire( 'execute' );
+			expect( balloon.visibleView ).to.equal( lastView );
+		} );
+
+		describe( 'integration with the editor selection (#render event)', () => {
+			it( 'should re-position the form', () => {
+				setData( model, '[<image src=""></image>]' );
+				button.fire( 'execute' );
+
+				const spy = sinon.spy( balloon, 'updatePosition' );
+
+				editor.editing.view.fire( 'render' );
+				sinon.assert.calledOnce( spy );
+			} );
+
+			it( 'should hide the form and focus editable when image widget has been removed by external change', () => {
+				setData( model, '[<image src=""></image>]' );
+				button.fire( 'execute' );
+
+				const removeSpy = sinon.spy( balloon, 'remove' );
+				const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+				// EnqueueChange automatically fires #render event.
+				model.enqueueChange( 'transparent', writer => {
+					writer.remove( doc.selection.getFirstRange() );
+				} );
+
+				sinon.assert.calledWithExactly( removeSpy, form );
+				sinon.assert.calledOnce( focusSpy );
+			} );
+		} );
+
+		describe( 'close listeners', () => {
+			describe( 'keyboard', () => {
+				it( 'should close upon Esc key press and focus the editing view', () => {
+					const hideSpy = sinon.spy( plugin, '_hideForm' );
+					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+					setData( model, '[<image src=""></image>]' );
+					button.fire( 'execute' );
+
+					const keyEvtData = {
+						keyCode: keyCodes.esc,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+
+					form.keystrokes.press( keyEvtData );
+					sinon.assert.calledOnce( hideSpy );
+					sinon.assert.calledOnce( keyEvtData.preventDefault );
+					sinon.assert.calledOnce( focusSpy );
+				} );
+			} );
+
+			describe( 'mouse', () => {
+				it( 'should close and not focus editable on click outside the panel', () => {
+					const hideSpy = sinon.spy( plugin, '_hideForm' );
+					const focusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+					setData( model, '[<image src=""></image>]' );
+					button.fire( 'execute' );
+
+					global.document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
+					sinon.assert.called( hideSpy );
+					sinon.assert.notCalled( focusSpy );
+				} );
+
+				it( 'should not close on click inside the panel', () => {
+					const spy = sinon.spy( plugin, '_hideForm' );
+
+					setData( model, '[<image src=""></image>]' );
+					button.fire( 'execute' );
+
+					form.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
+					sinon.assert.notCalled( spy );
+				} );
+			} );
+		} );
+	} );
+} );

+ 2 - 2
packages/ckeditor5-image/tests/imagetextalternative/ui/textalternativeformview.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -27,7 +27,7 @@ describe( 'TextAlternativeFormView', () => {
 		it( 'should create element from template', () => {
 			view.render();
 
-			expect( view.element.classList.contains( 'cke-text-alternative-form' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-text-alternative-form' ) ).to.be.true;
 			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 		} );
 

+ 25 - 26
packages/ckeditor5-image/tests/imagetoolbar.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -17,7 +17,7 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'ImageToolbar', () => {
-	let editor, doc, editingView, plugin, toolbar, balloon, editorElement;
+	let editor, model, doc, editingView, plugin, toolbar, balloon, editorElement;
 
 	beforeEach( () => {
 		editorElement = global.document.createElement( 'div' );
@@ -32,7 +32,8 @@ describe( 'ImageToolbar', () => {
 			} )
 			.then( newEditor => {
 				editor = newEditor;
-				doc = editor.document;
+				model = newEditor.model;
+				doc = model.document;
 				plugin = editor.plugins.get( ImageToolbar );
 				toolbar = plugin._toolbar;
 				editingView = editor.editing.view;
@@ -61,7 +62,7 @@ describe( 'ImageToolbar', () => {
 				expect( editor.plugins.get( ImageToolbar )._toolbar ).to.be.undefined;
 
 				editorElement.remove();
-				editor.destroy();
+				return editor.destroy();
 			} );
 	} );
 
@@ -76,13 +77,11 @@ describe( 'ImageToolbar', () => {
 
 			editor.ui.focusTracker.isFocused = true;
 
-			setData( doc, '[<image src=""></image>]' );
-
-			expect( toolbar.element.classList.contains( 'ck-editor-toolbar' ) ).to.be.true;
+			setData( model, '[<image src=""></image>]' );
 
 			sinon.assert.calledWithMatch( spy, {
 				view: toolbar,
-				balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container'
+				balloonClassName: 'ck-toolbar-container'
 			} );
 		} );
 	} );
@@ -91,7 +90,7 @@ describe( 'ImageToolbar', () => {
 		it( 'should show the toolbar when the editor gains focus and the image is selected', () => {
 			editor.ui.focusTracker.isFocused = true;
 
-			setData( doc, '[<image src=""></image>]' );
+			setData( model, '[<image src=""></image>]' );
 
 			editor.ui.focusTracker.isFocused = false;
 			expect( balloon.visibleView ).to.be.null;
@@ -103,7 +102,7 @@ describe( 'ImageToolbar', () => {
 		it( 'should hide the toolbar when the editor loses focus and the image is selected', () => {
 			editor.ui.focusTracker.isFocused = false;
 
-			setData( doc, '[<image src=""></image>]' );
+			setData( model, '[<image src=""></image>]' );
 
 			editor.ui.focusTracker.isFocused = true;
 			expect( balloon.visibleView ).to.equal( toolbar );
@@ -113,36 +112,36 @@ describe( 'ImageToolbar', () => {
 		} );
 	} );
 
-	describe( 'integration with the editor selection (#render event)', () => {
+	describe( 'integration with the editor selection (#change event)', () => {
 		beforeEach( () => {
 			editor.ui.focusTracker.isFocused = true;
 		} );
 
 		it( 'should show the toolbar on render when the image is selected', () => {
-			setData( doc, '<paragraph>[foo]</paragraph><image src=""></image>' );
+			setData( model, '<paragraph>[foo]</paragraph><image src=""></image>' );
 
 			expect( balloon.visibleView ).to.be.null;
 
-			editingView.fire( 'render' );
+			editingView.change( () => {} );
 			expect( balloon.visibleView ).to.be.null;
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				// Select the [<image></image>]
-				doc.selection.setRanges( [
+				writer.setSelection(
 					Range.createOn( doc.getRoot().getChild( 1 ) )
-				] );
+				);
 			} );
 
 			expect( balloon.visibleView ).to.equal( toolbar );
 
-			// Make sure successive render does not throw, e.g. attempting
+			// Make sure successive change does not throw, e.g. attempting
 			// to insert the toolbar twice.
-			editingView.fire( 'render' );
+			editingView.change( () => {} );
 			expect( balloon.visibleView ).to.equal( toolbar );
 		} );
 
 		it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
-			setData( doc, '[<image src=""></image>]' );
+			setData( model, '[<image src=""></image>]' );
 			expect( balloon.visibleView ).to.equal( toolbar );
 
 			const lastView = new View();
@@ -151,27 +150,27 @@ describe( 'ImageToolbar', () => {
 			balloon.add( { view: lastView } );
 			expect( balloon.visibleView ).to.equal( lastView );
 
-			editingView.fire( 'render' );
+			editingView.change( () => {} );
 			expect( balloon.visibleView ).to.equal( lastView );
 		} );
 
 		it( 'should hide the toolbar on render if the image is de–selected', () => {
-			setData( doc, '<paragraph>foo</paragraph>[<image src=""></image>]' );
+			setData( model, '<paragraph>foo</paragraph>[<image src=""></image>]' );
 
 			expect( balloon.visibleView ).to.equal( toolbar );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				// Select the <paragraph>[...]</paragraph>
-				doc.selection.setRanges( [
+				writer.setSelection(
 					Range.createIn( doc.getRoot().getChild( 0 ) )
-				] );
+				);
 			} );
 
 			expect( balloon.visibleView ).to.be.null;
 
-			// Make sure successive render does not throw, e.g. attempting
+			// Make sure successive change does not throw, e.g. attempting
 			// to remove the toolbar twice.
-			editingView.fire( 'render' );
+			editingView.change( () => {} );
 			expect( balloon.visibleView ).to.be.null;
 		} );
 	} );

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików