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

Merge branch 'master' into t/ckeditor5/1151

Aleksander Nowodzinski 6 лет назад
Родитель
Сommit
57451c72d8
65 измененных файлов с 2211 добавлено и 193 удалено
  1. 1 3
      packages/ckeditor5-ui/.travis.yml
  2. 19 0
      packages/ckeditor5-ui/CHANGELOG.md
  3. 1 2
      packages/ckeditor5-ui/README.md
  4. 4 1
      packages/ckeditor5-ui/lang/contexts.json
  5. 12 0
      packages/ckeditor5-ui/lang/translations/ar.po
  6. 12 0
      packages/ckeditor5-ui/lang/translations/ast.po
  7. 12 0
      packages/ckeditor5-ui/lang/translations/az.po
  8. 12 0
      packages/ckeditor5-ui/lang/translations/cs.po
  9. 12 0
      packages/ckeditor5-ui/lang/translations/da.po
  10. 12 0
      packages/ckeditor5-ui/lang/translations/de-ch.po
  11. 12 0
      packages/ckeditor5-ui/lang/translations/de.po
  12. 12 0
      packages/ckeditor5-ui/lang/translations/el.po
  13. 12 0
      packages/ckeditor5-ui/lang/translations/en-au.po
  14. 12 0
      packages/ckeditor5-ui/lang/translations/en.po
  15. 12 0
      packages/ckeditor5-ui/lang/translations/eo.po
  16. 12 0
      packages/ckeditor5-ui/lang/translations/es.po
  17. 12 0
      packages/ckeditor5-ui/lang/translations/et.po
  18. 12 0
      packages/ckeditor5-ui/lang/translations/eu.po
  19. 12 0
      packages/ckeditor5-ui/lang/translations/fa.po
  20. 12 0
      packages/ckeditor5-ui/lang/translations/fi.po
  21. 12 0
      packages/ckeditor5-ui/lang/translations/fr.po
  22. 12 0
      packages/ckeditor5-ui/lang/translations/gl.po
  23. 12 0
      packages/ckeditor5-ui/lang/translations/he.po
  24. 12 0
      packages/ckeditor5-ui/lang/translations/hr.po
  25. 12 0
      packages/ckeditor5-ui/lang/translations/hu.po
  26. 12 0
      packages/ckeditor5-ui/lang/translations/id.po
  27. 12 0
      packages/ckeditor5-ui/lang/translations/it.po
  28. 12 0
      packages/ckeditor5-ui/lang/translations/ja.po
  29. 12 0
      packages/ckeditor5-ui/lang/translations/km.po
  30. 12 0
      packages/ckeditor5-ui/lang/translations/kn.po
  31. 12 0
      packages/ckeditor5-ui/lang/translations/ku.po
  32. 41 0
      packages/ckeditor5-ui/lang/translations/lt.po
  33. 12 0
      packages/ckeditor5-ui/lang/translations/nb.po
  34. 12 0
      packages/ckeditor5-ui/lang/translations/ne.po
  35. 12 0
      packages/ckeditor5-ui/lang/translations/nl.po
  36. 12 0
      packages/ckeditor5-ui/lang/translations/no.po
  37. 12 0
      packages/ckeditor5-ui/lang/translations/pl.po
  38. 12 0
      packages/ckeditor5-ui/lang/translations/pt-br.po
  39. 12 0
      packages/ckeditor5-ui/lang/translations/pt.po
  40. 12 0
      packages/ckeditor5-ui/lang/translations/ro.po
  41. 12 0
      packages/ckeditor5-ui/lang/translations/ru.po
  42. 12 0
      packages/ckeditor5-ui/lang/translations/sk.po
  43. 12 0
      packages/ckeditor5-ui/lang/translations/sq.po
  44. 12 0
      packages/ckeditor5-ui/lang/translations/sr-latn.po
  45. 12 0
      packages/ckeditor5-ui/lang/translations/sr.po
  46. 12 0
      packages/ckeditor5-ui/lang/translations/sv.po
  47. 12 0
      packages/ckeditor5-ui/lang/translations/tr.po
  48. 12 0
      packages/ckeditor5-ui/lang/translations/ug.po
  49. 12 0
      packages/ckeditor5-ui/lang/translations/uk.po
  50. 12 0
      packages/ckeditor5-ui/lang/translations/zh-cn.po
  51. 12 0
      packages/ckeditor5-ui/lang/translations/zh.po
  52. 17 16
      packages/ckeditor5-ui/package.json
  53. 67 64
      packages/ckeditor5-ui/src/notification/notification.js
  54. 7 1
      packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js
  55. 597 68
      packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js
  56. 0 4
      packages/ckeditor5-ui/src/viewcollection.js
  57. 24 1
      packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.html
  58. 95 3
      packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.js
  59. 21 0
      packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.md
  60. 13 0
      packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js
  61. 710 30
      packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js
  62. 17 0
      packages/ckeditor5-ui/theme/components/panel/balloonrotator.css
  63. 23 0
      packages/ckeditor5-ui/theme/components/panel/fakepanel.css
  64. 1 0
      packages/ckeditor5-ui/theme/icons/next-arrow.svg
  65. 1 0
      packages/ckeditor5-ui/theme/icons/previous-arrow.svg

+ 1 - 3
packages/ckeditor5-ui/.travis.yml

@@ -28,6 +28,4 @@ after_script:
 - ckeditor5-dev-tests-notify-travis-status
 env:
   global:
-  - secure: W6VrPcKacfXYH/P5qjowRpGcq8UHwBpKIub7Tfj7pGaTfQHrUMbPb4jHJLQYd335eiIyVWzmwatYhRJwB5wtaPkfcByCqRqwXQsl3nkmEZYclQdhUgjhCJEPQlu93XNWkaITQnXaJzlJ5etzgyQBKVh+bmaTITCOli5rdLqO7Pge00boTDRevLZ+T6Fw4iNVwSPAwLpbgYfR7pujqPu6kulDrMSOKWcaMImuEGq9Bvps2LieF9KzvoNBCgoleZd+tFFtTvyQ1eE/UeTFQD8GXdKs7fSjj3evuyAm9FWqMBL+zT1EDJoTy4WljLJPwt6JRMazFSp9+j/0KNPuzksLTduGKAxjcc5OzbLxV3Ckh3A//szazAkgrJlK9BNYVLqj9lBns0nL2QbcyR8bY1w8mKVKIXVoCMAxuPWBYbEKKdODDUQXGbz/4wKxx2Ar0boo27v2ewBzo6BmwQYt7X4oTQTfkP7rpUHorKS9ztjHR0FiBAqLN4TQ0nCVqE93ajoGt5o9NO/ZTQbggMptym4JCkJle17K16kNPB96gkQkCIoa20n7/okkNclTl6g9NdD3oeWQMXz4S5Jx8lc3I/9kK5JqZL8KCTFZ//9UNuf6xaSf8g52usl+Kyv+A12u34SJ06n0KvmoA5aQfFHs4rioKL20e7RSI3edZqwp+THGTuk=
-  - secure: O0yOs6WG5YnrBbx7qyeG7sXsZW5pJN0ZvF/sZgHLWEZ4UYIxAGyW1FkM2v0nUroNo18v2X8IDaTOuSxBRXlJUlG8nPElHTIh/pJcVIXzDuVEW1sO4cQcqlUNAgIAK9g9y4aWBH0JXhSFeg1FwRP5Jjp73t9th7tMTjW3/qqOl+1rAjj6zXC+DAQEt8551ue2NZC5OEuIoPtfk57FBe1oAWyRSJIHNR5dj10E9JXlrdVJzcCFF25ikYQbhjl3RpIiFAdaKKK68GyPpx5hkb0rBDHyHcIYSanFZSY2cAKkhdKrjFqWN97scQEK3NtLepOt6cKtH69KG0mYgyKvkIQgoyXo9RDW3NUEO4mQwd2ADOBRwvda5ekFdaQtCr1Dznuw+wrNeHAg/uI/qBBJYJ3ZuZFcD5DTg/zbPtOQzZb/LuhJuOYMVBvQyG4X6IUfbK3yLooo8gZjVWoxyb4u9S/fe55gArzFE5aWbjBDdTNw5qVDu2qd21yd9B6ol6QupT+aCRoKBtgUEU1VeUWdVs+RkLdi+f+kJ11FPA/RwG4D6EBgTd4c1JRHkUxSQNVvkPM7lb/x+IoHtOakjQTVUE/C459KigW+kD4iMlUPrFHND0tC34K2E67wWV1O7KfStD84NqgFwX5V/48EUSrqyePpMegOIet6D36JXRh+NssU5rU=
-  - secure: vONyRek0j6i2EWZPClfVRfoAQUxvARM2JfwPsCyLO6ORhJZEsVH6Ky6++1dcrRkS17Z4PzJ2Fi/EZ7CVn0KI1NIZEQll5rxlvdblm5SfMwRSJiUflnGbJTNTsOTLqlU6hA18jmOLmf5aLR6PMeh/AD1XPsvBxNrBgUNR2wJihvs9P2gcnyj0QYe4IJk1HSqanXOESqo+87qSQGGuJSaTVIIH1DxqrE5aSBZfghIHyEoGl43x+MxjIcZJqqDfY2JOOEjowGHt+dGOeLHI7uUZIGbQO0sy8QgnFbSN0cg97b+H+y54PY3qTlGl4rOxe1MBavEIbc5tQKyUXSjC1Aq/5nYKbQJN0Tb7/DgSrTOfX1xk6N1Fe06ptuWhKi5rP22/tq7GIM4+4O/E0YkVpKbESdMfCiamOcipkrUlhNfCa7SE1WXDLbAnIa2kSw76uXveUOueyU2F9CTleTY25RV5fOjWXYCJduM46LKNZj2q97i7FKC9bl9miPCBi9zQFm9M6DT9h1i2vpHvhxyIgy/BPVrw9nWqx8D/Ci4ayQp2d2M7VfREeiIGDpOH5XE3+/t/E99Sn25o9unKqGuUAIjABsduxhfujLIOMWXvSFwXUfs07k4EYQ5ga+5Qjgmm2VY9berW9kRtdUaiA/s0kiEek4mvlTdRlHG+jNvuFqEup0I=
+  - secure: rU6Tu6fffAXbs3vEbJIa9MN5foP02BiEC5iFWvVI9qEyOhHsDq69n6v3/qco579L/bvNZw60eB59oFFIlKsH1NqVsNipXppuC8ez4nwGG+3dPx1pr/VAZwoFYrodSIkbdcbf4yM783yEVE0eQBr8S/w2efqXutjxc7wzjk+Jg3aITIsSekbEfq+8Cs/bD3/0qCwOa9C3Vd9Sjn+TA+Lahkt/gbofKHrE1teFiR38R/E2I+q7pqyLIy0ZSCQnjYYcThc74q0tbiNExbouWMUOUaCPBo3FLG9o1aO0oPerMsinY5USvkWMuEdKzlzsFyfCYgwsYgHxA4Xzd9+E/hNNin7EBenRIaKIzMytYmo+SsS1YAj83n84j3eGPwmjiDfkWegY7o8PaOvP7D9cYUwMFp9gLMvFNGXNF2cHvZ2HBHle+Gr/beIGE72jYIJGntq9R+/8lqddekTIw18qPBsInrklFfGxSN60AZikhd9reqrxss8fVai6U6Yq8WgCXWx5Z54onG+FtJimcwDBxqTgu6wxpRl3B/v9L51WIYzbPmMXObAY/s5S7E9o/gw1qsP8cBxQ7UxElq/Tm4C52sopkJrTdUiADPhBq4ZrAdb075RWS2kA4YldnRCvyQPTfd4PWJfedD+qJFIVBfSoxCZIxohBP2J/y2INnFZEGXjE9wI=

+ 19 - 0
packages/ckeditor5-ui/CHANGELOG.md

@@ -1,6 +1,25 @@
 Changelog
 =========
 
+## [13.0.0](https://github.com/ckeditor/ckeditor5-ui/compare/v12.1.0...v13.0.0) (2019-06-05)
+
+### Features
+
+* Implemented the single view mode for the `ContextualBalloon` plugin. See https://github.com/ckeditor/ckeditor5-mention/issues/74. ([c000c93](https://github.com/ckeditor/ckeditor5-ui/commit/c000c93))
+* Introduced rotatable panels for `ContextualBalloon`. Closes https://github.com/ckeditor/ckeditor5-ui/issues/491. ([581d8f7](https://github.com/ckeditor/ckeditor5-ui/commit/581d8f7))
+
+### Other changes
+
+* Renamed a misspelled `InputTextViewariaDesribedById` property to `InputTextView#ariaDescribedById`. Closes [#483](https://github.com/ckeditor/ckeditor5-ui/issues/483). ([2288bf3](https://github.com/ckeditor/ckeditor5-ui/commit/2288bf3))
+* The `_rwd.css` mixin was moved to this package from `@ckeditor/ckeditor5-theme-lark`. See [ckeditor/ckeditor5#1662](https://github.com/ckeditor/ckeditor5/issues/1662). ([96e3a3c](https://github.com/ckeditor/ckeditor5-ui/commit/96e3a3c))
+* The `ContextualBalloon#add()` method should accept the `withArrow` option. Closes [#487](https://github.com/ckeditor/ckeditor5-ui/issues/487). ([0e7f670](https://github.com/ckeditor/ckeditor5-ui/commit/0e7f670))
+* Updated translations. ([4699d8b](https://github.com/ckeditor/ckeditor5-ui/commit/4699d8b)) ([d4c5714](https://github.com/ckeditor/ckeditor5-ui/commit/d4c5714))
+
+### BREAKING CHANGES
+
+* The (misspelled) `InputTextView#ariaDesribedById` property is no longer available. Use `ariaDescribedById` instead.
+
+
 ## [12.1.0](https://github.com/ckeditor/ckeditor5-ui/compare/v12.0.0...v12.1.0) (2019-04-10)
 
 ### Features

+ 1 - 2
packages/ckeditor5-ui/README.md

@@ -4,7 +4,6 @@ CKEditor 5 UI framework
 [![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-ui.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-ui.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-ui)
-[![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-ui/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-ui?branch=master)
 <br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-ui/status.svg)](https://david-dm.org/ckeditor/ckeditor5-ui)
@@ -18,4 +17,4 @@ See the [`@ckeditor/ckeditor5-ui` package](https://ckeditor.com/docs/ckeditor5/l
 
 ## License
 
-Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file.
+Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).

+ 4 - 1
packages/ckeditor5-ui/lang/contexts.json

@@ -1,5 +1,8 @@
 {
 	"Rich Text Editor, %0": "Title of the CKEditor5 editor.",
 	"Rich Text Editor": "Title of the CKEditor5 editor.",
-	"Edit block": "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
+	"Edit block": "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)",
+	"Next": "Label for a button showing the next thing (tab, page, etc.).",
+	"Previous": "Label for a button showing the previous thing (tab, page, etc.).",
+	"%0 of %1": "Label for a 'page X of Y' status of a typical next/previous page navigation."
 }

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ar.po

@@ -27,3 +27,15 @@ msgstr "معالج نصوص"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ast.po

@@ -27,3 +27,15 @@ msgstr "Editor de testu arriquecíu"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/az.po

@@ -27,3 +27,15 @@ msgstr "Rich Text Redaktoru"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Redaktə etmək bloku"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/cs.po

@@ -27,3 +27,15 @@ msgstr "Textový editor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/da.po

@@ -27,3 +27,15 @@ msgstr "Wysiwyg editor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/de-ch.po

@@ -27,3 +27,15 @@ msgstr "Rich-Text-Edito"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/de.po

@@ -27,3 +27,15 @@ msgstr "Rich Text Editor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Absatz bearbeiten"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/el.po

@@ -27,3 +27,15 @@ msgstr "Επεξεργαστής Πλούσιου Κειμένου"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/en-au.po

@@ -26,4 +26,16 @@ msgstr "Rich Text Editor"
 
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
+msgstr "Edit block"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
 msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/en.po

@@ -27,3 +27,15 @@ msgstr "Rich Text Editor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Edit block"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr "Next"
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr "Previous"
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr "%0 of %1"

+ 12 - 0
packages/ckeditor5-ui/lang/translations/eo.po

@@ -27,3 +27,15 @@ msgstr "Redaktilo de Riĉa Teksto"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/es.po

@@ -27,3 +27,15 @@ msgstr "Editor de Texto Enriquecido"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Cuadro de edición"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/et.po

@@ -27,3 +27,15 @@ msgstr "Tekstiredaktor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Muuda plokki"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/eu.po

@@ -27,3 +27,15 @@ msgstr "Testu aberastuaren editorea"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/fa.po

@@ -27,3 +27,15 @@ msgstr "ویرایشگر متن غنی"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "ویرایش قطعه"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/fi.po

@@ -26,4 +26,16 @@ msgstr "Rikas tekstieditori"
 
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
+msgstr "Muokkaa lohkoa"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
 msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/fr.po

@@ -27,3 +27,15 @@ msgstr "Éditeur de texte enrichi"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Modifier le bloc"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/gl.po

@@ -27,3 +27,15 @@ msgstr "Editor de texto mellorado"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Editar bloque"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/he.po

@@ -27,3 +27,15 @@ msgstr "עורך טקסט עשיר"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/hr.po

@@ -27,3 +27,15 @@ msgstr "Rich Text Editor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/hu.po

@@ -27,3 +27,15 @@ msgstr "Bővített szövegszerkesztő"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Blokk szerkesztése"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/id.po

@@ -27,3 +27,15 @@ msgstr "Editor Teks Kaya"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Sunting blok"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/it.po

@@ -27,3 +27,15 @@ msgstr "Editor di testo formattato"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Modifica blocco"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ja.po

@@ -27,3 +27,15 @@ msgstr "リッチテキストエディター"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/km.po

@@ -27,3 +27,15 @@ msgstr "កម្មវិធី​កែសម្រួល​អត្ថបទ
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/kn.po

@@ -27,3 +27,15 @@ msgstr "‍ಸಮೃದ್ಧ ಪಠ್ಯ ಸಂಪಾದಕ‍‍"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ku.po

@@ -27,3 +27,15 @@ msgstr "سەرنوسەری دەقی بەپیت"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "دەستکاری بلۆک"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 41 - 0
packages/ckeditor5-ui/lang/translations/lt.po

@@ -0,0 +1,41 @@
+# Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+#
+#                                     !!! IMPORTANT !!!
+#
+#         Before you edit this file, please keep in mind that contributing to the project
+#                translations is possible ONLY via the Transifex online service.
+#
+#         To submit your translations, visit https://www.transifex.com/ckeditor/ckeditor5.
+#
+#                   To learn more, check out the official contributor's guide:
+#     https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html
+#
+msgid ""
+msgstr ""
+"Language-Team: Lithuanian (https://www.transifex.com/ckeditor/teams/11143/lt/)\n"
+"Language: lt\n"
+"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n"
+
+msgctxt "Title of the CKEditor5 editor."
+msgid "Rich Text Editor, %0"
+msgstr "Raiškiojo teksto redaktorius, %0"
+
+msgctxt "Title of the CKEditor5 editor."
+msgid "Rich Text Editor"
+msgstr "Raiškiojo teksto redaktorius"
+
+msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
+msgid "Edit block"
+msgstr "Redaguoti bloką"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/nb.po

@@ -27,3 +27,15 @@ msgstr "Rikteksteditor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ne.po

@@ -26,4 +26,16 @@ msgstr "धनी पाठ सम्पादक"
 
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
+msgstr "ब्लक सम्पादन गर्न"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
 msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/nl.po

@@ -27,3 +27,15 @@ msgstr "Tekstbewerker"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/no.po

@@ -27,3 +27,15 @@ msgstr "Tekstredigeringsverktøy for rik tekst"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Rediger blokk"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/pl.po

@@ -27,3 +27,15 @@ msgstr "Edytor tekstu RTF"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Edytuj block"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/pt-br.po

@@ -27,3 +27,15 @@ msgstr "Editor de Formatação"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Editor de bloco"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/pt.po

@@ -27,3 +27,15 @@ msgstr "Editor de texto avançado"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ro.po

@@ -27,3 +27,15 @@ msgstr "Editor de text îmbunătățit"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ru.po

@@ -26,4 +26,16 @@ msgstr "Редактор"
 
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
+msgstr "Редактировать блок"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
 msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/sk.po

@@ -27,3 +27,15 @@ msgstr "Editor s formátovaním"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/sq.po

@@ -27,3 +27,15 @@ msgstr "Redaktues i Tekstit të Pasur"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/sr-latn.po

@@ -27,3 +27,15 @@ msgstr "Prošireni uređivač teksta"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Blok uređivač"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/sr.po

@@ -27,3 +27,15 @@ msgstr "Проширен уређивач текста"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Блок уређивач"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/sv.po

@@ -27,3 +27,15 @@ msgstr "Rich Text-editor"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/tr.po

@@ -27,3 +27,15 @@ msgstr "Zengin İçerik Editörü"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Bloğu Düzenle"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/ug.po

@@ -27,3 +27,15 @@ msgstr "تېكىست تەھرىرلىگۈچ"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr ""
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/uk.po

@@ -27,3 +27,15 @@ msgstr "Розширений текстовий редактор"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "Редагувати блок"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/zh-cn.po

@@ -27,3 +27,15 @@ msgstr "富文本编辑器"
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
 msgstr "编辑框"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
+msgstr ""

+ 12 - 0
packages/ckeditor5-ui/lang/translations/zh.po

@@ -26,4 +26,16 @@ msgstr "豐富文字編輯器"
 
 msgctxt "Label of the block toolbar icon (a block toolbar is displayed next to each paragraph, heading, list item, etc. and contains e.g. block formatting options)"
 msgid "Edit block"
+msgstr "編輯區塊"
+
+msgctxt "Label for a button showing the next thing (tab, page, etc.)."
+msgid "Next"
+msgstr ""
+
+msgctxt "Label for a button showing the previous thing (tab, page, etc.)."
+msgid "Previous"
+msgstr ""
+
+msgctxt "Label for a 'page X of Y' status of a typical next/previous page navigation."
+msgid "%0 of %1"
 msgstr ""

+ 17 - 16
packages/ckeditor5-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-ui",
-  "version": "12.1.0",
+  "version": "13.0.0",
   "description": "The UI framework and standard UI library of CKEditor 5.",
   "keywords": [
     "ckeditor",
@@ -9,24 +9,25 @@
     "ckeditor5-lib"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^12.1.0",
-    "@ckeditor/ckeditor5-utils": "^12.1.0",
+    "@ckeditor/ckeditor5-core": "^12.1.1",
+    "@ckeditor/ckeditor5-utils": "^12.1.1",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.0",
-    "@ckeditor/ckeditor5-block-quote": "^11.0.1",
-    "@ckeditor/ckeditor5-editor-balloon": "^12.1.0",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.0",
-    "@ckeditor/ckeditor5-engine": "^13.1.0",
-    "@ckeditor/ckeditor5-enter": "^11.0.1",
-    "@ckeditor/ckeditor5-essentials": "^11.0.1",
-    "@ckeditor/ckeditor5-heading": "^11.0.1",
-    "@ckeditor/ckeditor5-image": "^13.0.1",
-    "@ckeditor/ckeditor5-link": "^11.0.1",
-    "@ckeditor/ckeditor5-list": "^12.0.1",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.1",
-    "@ckeditor/ckeditor5-typing": "^12.0.1",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.1",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.0",
+    "@ckeditor/ckeditor5-editor-balloon": "^12.1.1",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.1",
+    "@ckeditor/ckeditor5-engine": "^13.1.1",
+    "@ckeditor/ckeditor5-enter": "^11.0.2",
+    "@ckeditor/ckeditor5-essentials": "^11.0.2",
+    "@ckeditor/ckeditor5-heading": "^11.0.2",
+    "@ckeditor/ckeditor5-image": "^13.1.0",
+    "@ckeditor/ckeditor5-link": "^11.0.2",
+    "@ckeditor/ckeditor5-list": "^12.0.2",
+    "@ckeditor/ckeditor5-mention": "^11.0.0",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.2",
+    "@ckeditor/ckeditor5-typing": "^12.0.2",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^1.0.11",
     "husky": "^1.3.1",

+ 67 - 64
packages/ckeditor5-ui/src/notification/notification.js

@@ -14,11 +14,11 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 /**
  * The Notification plugin.
  *
- * This plugin sends few base types of notifications: `success`, `info` and `warning`. This notifications need to be
- * handled and displayed by plugin responsible for showing UI of the notifications. Using this plugin for dispatching
- * notifications makes possible to switch the notifications UI.
+ * This plugin sends a few types of notifications: `success`, `info` and `warning`. The notifications need to be
+ * handled and displayed by a plugin responsible for showing the UI of the notifications. Using this plugin for dispatching
+ * notifications makes it possible to switch the notifications UI.
  *
- * Note that every unhandled and not stopped `warning` notification will be displayed as system alert.
+ * Note that every unhandled and not stopped `warning` notification will be displayed as a system alert.
  * See {@link module:ui/notification/notification~Notification#showWarning}.
  *
  * @extends module:core/plugin~Plugin
@@ -35,33 +35,34 @@ export default class Notification extends Plugin {
 	 * @inheritDoc
 	 */
 	init() {
-		// Each unhandled and not stopped `show:warning` event is displayed as system alert.
+		// Each unhandled and not stopped `show:warning` event is displayed as a system alert.
 		this.on( 'show:warning', ( evt, data ) => {
 			window.alert( data.message ); // eslint-disable-line no-alert
 		}, { priority: 'lowest' } );
 	}
 
 	/**
-	 * Shows success notification.
+	 * Shows a success notification.
 	 *
-	 * At default it fires `show:success` event with given data but event namespace can be extended
-	 * by `data.namespace` option e.g.
+	 * By default, it fires the {@link #event:show:success `show:success` event} with the given `data`. The event namespace can be extended
+	 * using the `data.namespace` option. For example:
 	 *
 	 * 		showSuccess( 'Image is uploaded.', {
 	 * 			namespace: 'upload:image'
 	 * 		} );
 	 *
-	 * will fire `show:success:upload:image` event.
-	 * Title of the notification can be provided:
+	 * will fire the `show:success:upload:image` event.
+	 *
+	 * You can provide the title of the notification:
 	 *
 	 *		showSuccess( 'Image is uploaded.', {
 	 *			title: 'Image upload success'
-	 *		});
+	 *		} );
 	 *
-	 * @param {String} message Content of the notification.
+	 * @param {String} message The content of the notification.
 	 * @param {Object} [data={}] Additional data.
 	 * @param {String} [data.namespace] Additional event namespace.
-	 * @param {String} [data.title] Title of the notification.
+	 * @param {String} [data.title] The title of the notification.
 	 */
 	showSuccess( message, data = {} ) {
 		this._showNotification( {
@@ -73,26 +74,27 @@ export default class Notification extends Plugin {
 	}
 
 	/**
-	 * Shows info notification.
+	 * Shows an information notification.
 	 *
-	 * At default it fires `show:info` event with given data but event namespace can be extended
-	 * by `data.namespace` option e.g.
+	 * By default, it fires the {@link #event:show:info `show:info` event} with the given `data`. The event namespace can be extended
+	 * using the `data.namespace` option. For example:
 	 *
 	 * 		showInfo( 'Editor is offline.', {
 	 * 			namespace: 'editor:status'
 	 * 		} );
 	 *
-	 * will fire `show:info:editor:status` event.
-	 * Title of the notification can be provided:
+	 * will fire the `show:info:editor:status` event.
+	 *
+	 * You can provide the title of the notification:
 	 *
 	 *		showInfo( 'Editor is offline.', {
 	 *			title: 'Network information'
-	 *		});
+	 *		} );
 	 *
-	 * @param {String} message Content of the notification.
+	 * @param {String} message The content of the notification.
 	 * @param {Object} [data={}] Additional data.
 	 * @param {String} [data.namespace] Additional event namespace.
-	 * @param {String} [data.title] Title of the notification.
+	 * @param {String} [data.title] The title of the notification.
 	 */
 	showInfo( message, data = {} ) {
 		this._showNotification( {
@@ -104,49 +106,50 @@ export default class Notification extends Plugin {
 	}
 
 	/**
-	 * Shows warning notification.
+	 * Shows a warning notification.
 	 *
-	 * At default it fires `show:warning` event with given data but event namespace can be extended
-	 * by `data.namespace` option e.g.
+	 * By default, it fires the {@link #event:show:warning `show:warning` event}
+	 * with the given `data`. The event namespace can be extended using the `data.namespace` option. For example:
 	 *
 	 * 		showWarning( 'Image upload error.', {
 	 * 			namespace: 'upload:image'
 	 * 		} );
 	 *
-	 * will fire `show:warning:upload:image` event.
-	 * Title of the notification can be provided:
+	 * will fire the `show:warning:upload:image` event.
+	 *
+	 * You can provide the title of the notification:
 	 *
 	 *		showWarning( 'Image upload error.', {
 	 *			title: 'Upload failed'
-	 *		});
+	 *		} );
 	 *
-	 * Note that each unhandled and not stopped `warning` notification will be displayed as system alert.
-	 * Plugin responsible for displaying warnings should `stop()` the event to prevent of displaying it as alert:
+	 * Note that each unhandled and not stopped `warning` notification will be displayed as a system alert.
+	 * The plugin responsible for displaying warnings should `stop()` the event to prevent displaying it as an alert:
 	 *
 	 * 		notifications.on( 'show:warning', ( evt, data ) => {
-	 * 			// Do something with data.
+	 * 			// Do something with the data.
 	 *
-	 * 			// Stop this event to prevent of displaying as alert.
+	 * 			// Stop this event to prevent displaying it as an alert.
 	 * 			evt.stop();
 	 * 		} );
 	 *
-	 * You can attach many listeners to the same event and `stop()` this event in the listener with the low priority:
+	 * You can attach many listeners to the same event and `stop()` this event in a listener with a low priority:
 	 *
 	 * 		notifications.on( 'show:warning', ( evt, data ) => {
-	 * 			// Show warning in the UI, but not stop it.
+	 * 			// Show the warning in the UI, but do not stop it.
 	 * 		} );
 	 *
 	 * 		notifications.on( 'show:warning', ( evt, data ) => {
-	 * 			// Log warning to some error tracker.
+	 * 			// Log the warning to some error tracker.
 	 *
-	 * 			// Stop this event to prevent of displaying as alert.
+	 * 			// Stop this event to prevent displaying it as an alert.
 	 * 			evt.stop();
 	 * 		}, { priority: 'low' } );
 	 *
-	 * @param {String} message Content of the notification.
+	 * @param {String} message The content of the notification.
 	 * @param {Object} [data={}] Additional data.
 	 * @param {String} [data.namespace] Additional event namespace.
-	 * @param {String} [data.title] Title of the notification.
+	 * @param {String} [data.title] The title of the notification.
 	 */
 	showWarning( message, data = {} ) {
 		this._showNotification( {
@@ -158,14 +161,14 @@ export default class Notification extends Plugin {
 	}
 
 	/**
-	 * Fires `show` event with specified type, namespace and message.
+	 * Fires the `show` event with the specified type, namespace and message.
 	 *
 	 * @private
-	 * @param {Object} data Message data.
-	 * @param {String} data.message Content of the notification.
-	 * @param {'success'|'info'|'warning'} data.type Type of message.
+	 * @param {Object} data The message data.
+	 * @param {String} data.message The content of the notification.
+	 * @param {'success'|'info'|'warning'} data.type The type of the message.
 	 * @param {String} [data.namespace] Additional event namespace.
-	 * @param {String} [data.title=''] Title of the notification.
+	 * @param {String} [data.title=''] The title of the notification.
 	 */
 	_showNotification( data ) {
 		const event = `show:${ data.type }` + ( data.namespace ? `:${ data.namespace }` : '' );
@@ -178,45 +181,45 @@ export default class Notification extends Plugin {
 	}
 
 	/**
-	 * Fired when one of `showSuccess`, `showInfo`, `showWarning` methods is called.
+	 * Fired when one of the `showSuccess()`, `showInfo()`, `showWarning()` methods is called.
 	 *
 	 * @event show
-	 * @param {Object} data Notification data.
-	 * @param {String} data.message Content of the notification.
-	 * @param {String} data.title Title of the notification.
-	 * @param {'success'|'info'|'warning'} data.type Type of notification.
+	 * @param {Object} data The notification data.
+	 * @param {String} data.message The content of the notification.
+	 * @param {String} data.title The title of the notification.
+	 * @param {'success'|'info'|'warning'} data.type The type of the notification.
 	 */
 
 	/**
-	 * Fired when `showSuccess` method is called.
+	 * Fired when the `showSuccess()` method is called.
 	 *
 	 * @event show:success
-	 * @param {Object} data Notification data.
-	 * @param {String} data.message Content of the notification.
-	 * @param {String} data.title Title of the notification.
-	 * @param {'success'} data.type Type of notification.
+	 * @param {Object} data The notification data.
+	 * @param {String} data.message The content of the notification.
+	 * @param {String} data.title The title of the notification.
+	 * @param {'success'} data.type The type of the notification.
 	 */
 
 	/**
-	 * Fired when `showInfo` method is called.
+	 * Fired when the `showInfo()` method is called.
 	 *
 	 * @event show:info
-	 * @param {Object} data Notification data.
-	 * @param {String} data.message Content of the notification.
-	 * @param {String} data.title Title of the notification.
-	 * @param {'info'} data.type Type of notification.
+	 * @param {Object} data The notification data.
+	 * @param {String} data.message The content of the notification.
+	 * @param {String} data.title The title of the notification.
+	 * @param {'info'} data.type The type of the notification.
 	 */
 
 	/**
-	 * Fired when `showWarning` method is called.
+	 * Fired when the `showWarning()` method is called.
 	 *
-	 * When this event won't be handled and stopped by `event.stop()` then data.message of this event will
-	 * be automatically displayed as system alert.
+	 * When this event is not handled or stopped by `event.stop()`, the `data.message` of this event will
+	 * be automatically displayed as a system alert.
 	 *
 	 * @event show:warning
-	 * @param {Object} data Notification data.
-	 * @param {String} data.message Content of the notification.
-	 * @param {String} data.title Title of the notification.
-	 * @param {'warning'} data.type Type of notification.
+	 * @param {Object} data The notification data.
+	 * @param {String} data.message The content of the notification.
+	 * @param {String} data.title The title of the notification.
+	 * @param {'warning'} data.type The type of the notification.
 	 */
 }

+ 7 - 1
packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js

@@ -236,7 +236,13 @@ export default class BalloonPanelView extends View {
 			fitInViewport: true
 		}, options );
 
-		const { top, left, name: position } = BalloonPanelView._getOptimalPosition( positionOptions );
+		const optimalPosition = BalloonPanelView._getOptimalPosition( positionOptions );
+
+		// Usually browsers make some problems with super accurate values like 104.345px
+		// so it is better to use int values.
+		const left = parseInt( optimalPosition.left );
+		const top = parseInt( optimalPosition.top );
+		const position = optimalPosition.name;
 
 		Object.assign( this, { top, left, position } );
 	}

+ 597 - 68
packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js

@@ -9,22 +9,54 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import BalloonPanelView from './balloonpanelview';
+import View from '../../view';
+import ButtonView from '../../button/buttonview';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+
+import prevIcon from '../../../theme/icons/previous-arrow.svg';
+import nextIcon from '../../../theme/icons/next-arrow.svg';
+
+import '../../../theme/components/panel/balloonrotator.css';
+import '../../../theme/components/panel/fakepanel.css';
+
+const toPx = toUnit( 'px' );
 
 /**
- * Provides the common contextual balloon panel for the editor.
- *
- * This plugin allows reusing a single {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance
- * to display multiple contextual balloon panels in the editor.
+ * Provides the common contextual balloon for the editor.
  *
- * Child views of such a panel are stored in the stack and the last one in the stack is visible. When the
- * visible view is removed from the stack, the previous view becomes visible, etc. If there are no more
- * views in the stack, the balloon panel will hide.
- *
- * It simplifies managing the views and helps
+ * The role of this plugin is to unified contextual balloons logic, simplifies managing the views and helps
  * avoid the unnecessary complexity of handling multiple {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
  * instances in the editor.
  *
+ * This plugin allows creating single or multiple panel stacks.
+ *
+ * Each stack may have multiple views, the one on the top is visible. When the visible view is removed from the stack,
+ * the previous view becomes visible, etc. It might be useful to implement nested navigation in a balloon. For instance
+ * toolbar view may have a link button. When you click it, link view (which let you set the URL) is created and put on
+ * top of the toolbar view, so not link panel is displayed. When you finish editing link and close (remove) link view,
+ * the toolbar view is visible back.
+ *
+ * However, there are cases when there are multiple independent balloons to be displayed. For instance, if the selection
+ * is inside two inline comments at the same time. For such cases, you can create two independent panel stacks.
+ * Contextual balloon plugin will create a navigation bar to let users switch between these panel stacks - "next balloon"
+ * and "previous balloon" buttons.
+ *
+ * If there are no views in the current stack, the balloon panel will try to switch to the next stack. If there are no
+ * panels in any stack then balloon panel will be hidden.
+ *
+ * **Note**: To force balloon panel to show only one view - even if there are other stacks - use `singleViewMode=true` option
+ * when {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#add adding} view to a panel.
+ *
+ * From the implementation point of view, contextual ballon plugin is reusing a single
+ * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance to display multiple contextual balloon
+ * panels in the editor. It also creates a special {@link module:ui/panel/balloon/contextualballoon~RotatorView rotator view},
+ * used to manage multiple panel stacks. Rotator view is a child of the balloon panel view and the parent of the specific
+ * view you want to display. If there is are more than one panel stack to be displayed, the rotator view will add a
+ * navigation bar. If there is only one stack, rotator view is transparent (do not add any UI elements).
+ *
  * @extends module:core/plugin~Plugin
  */
 export default class ContextualBalloon extends Plugin {
@@ -38,14 +70,8 @@ export default class ContextualBalloon extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
-	init() {
-		/**
-		 * The common balloon panel view.
-		 *
-		 * @readonly
-		 * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
-		 */
-		this.view = new BalloonPanelView();
+	constructor( editor ) {
+		super( editor );
 
 		/**
 		 * The {@link module:utils/dom/position~Options#limiter position limiter}
@@ -71,51 +97,99 @@ export default class ContextualBalloon extends Plugin {
 		};
 
 		/**
-		 * Stack of the views injected into the balloon. Last one in the stack is displayed
-		 * as a content of {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#view}.
+		 * The currently visible view or `null` when there are no views in the any stack.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {module:ui/view~View|null} #visibleView
+		 */
+		this.set( 'visibleView', null );
+
+		/**
+		 * The common balloon panel view.
+		 *
+		 * @readonly
+		 * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
+		 */
+		this.view = new BalloonPanelView( editor.locale );
+		editor.ui.view.body.add( this.view );
+		editor.ui.focusTracker.add( this.view.element );
+
+		/**
+		 * Map of views and its stacks.
 		 *
 		 * @private
-		 * @member {Map} #_stack
+		 * @type {Map.<module:ui/view~View,Set>}
 		 */
-		this._stack = new Map();
+		this._viewToStack = new Map();
 
-		// Add balloon panel view to editor `body` collection and wait until view will be ready.
-		this.editor.ui.view.body.add( this.view );
+		/**
+		 * Map of ids and stacks.
+		 *
+		 * @private
+		 * @type {Map.<String,Set>}
+		 */
+		this._idToStack = new Map();
 
-		// Editor should be focused when contextual balloon is focused.
-		this.editor.ui.focusTracker.add( this.view.element );
-	}
+		/**
+		 * A total number of all stacks in the balloon.
+		 *
+		 * @private
+		 * @readonly
+		 * @observable
+		 * @member {Number} #_numberOfStacks
+		 */
+		this.set( '_numberOfStacks', 0 );
 
-	/**
-	 * Returns the currently visible view or `null` when there are no
-	 * views in the stack.
-	 *
-	 * @returns {module:ui/view~View|null}
-	 */
-	get visibleView() {
-		const item = this._stack.get( this.view.content.get( 0 ) );
+		/**
+		 * Flag that controls the single view mode.
+		 *
+		 * @private
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #_singleViewMode
+		 */
+		this.set( '_singleViewMode', false );
+
+		/**
+		 * Rotator view embedded in the contextual balloon.
+		 * Displays currently visible view in the balloon and provides navigation for switching stacks.
+		 *
+		 * @private
+		 * @type {module:ui/panel/balloon/contextualballoon~RotatorView}
+		 */
+		this._rotatorView = this._createRotatorView();
 
-		return item ? item.view : null;
+		/**
+		 * Displays fake panels under the balloon panel view when multiple stacks are added to the balloon.
+		 *
+		 * @private
+		 * @type {module:ui/view~View}
+		 */
+		this._fakePanelsView = this._createFakePanelsView();
 	}
 
 	/**
-	 * Returns `true` when the given view is in the stack. Otherwise returns `false`.
+	 * Returns `true` when the given view is in one of the stack. Otherwise returns `false`.
 	 *
 	 * @param {module:ui/view~View} view
 	 * @returns {Boolean}
 	 */
 	hasView( view ) {
-		return this._stack.has( view );
+		return Array.from( this._viewToStack.keys() ).includes( view );
 	}
 
 	/**
-	 * Adds a new view to the stack and makes it visible.
+	 * Adds a new view to the stack and makes it visible if current stack is visible
+	 * or it is a first view in the balloon.
 	 *
 	 * @param {Object} data Configuration of the view.
+	 * @param {String} [data.stackId='main'] Id of a stack that view is added to.
 	 * @param {module:ui/view~View} [data.view] Content of the balloon.
 	 * @param {module:utils/dom/position~Options} [data.position] Positioning options.
 	 * @param {String} [data.balloonClassName] Additional CSS class added to the {@link #view balloon} when visible.
 	 * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
+	 * @param {Boolean} [data.singleViewMode=false] Whether the view should be only visible view - even if other stacks were added.
 	 */
 	add( data ) {
 		if ( this.hasView( data.view ) ) {
@@ -127,23 +201,42 @@ export default class ContextualBalloon extends Plugin {
 			throw new CKEditorError( 'contextualballoon-add-view-exist: Cannot add configuration of the same view twice.' );
 		}
 
-		// When adding view to the not empty balloon.
-		if ( this.visibleView ) {
-			// Remove displayed content from the view.
-			this.view.content.remove( this.visibleView );
+		const stackId = data.stackId || 'main';
+
+		// If new stack is added, creates it and show view from this stack.
+		if ( !this._idToStack.has( stackId ) ) {
+			this._idToStack.set( stackId, new Map( [ [ data.view, data ] ] ) );
+			this._viewToStack.set( data.view, this._idToStack.get( stackId ) );
+			this._numberOfStacks = this._idToStack.size;
+
+			if ( !this._visibleStack || data.singleViewMode ) {
+				this.showStack( stackId );
+			}
+
+			return;
+		}
+
+		const stack = this._idToStack.get( stackId );
+
+		if ( data.singleViewMode ) {
+			this.showStack( stackId );
 		}
 
 		// Add new view to the stack.
-		this._stack.set( data.view, data );
+		stack.set( data.view, data );
+		this._viewToStack.set( data.view, stack );
 
-		// And display it.
-		this._show( data );
+		// And display it if is added to the currently visible stack.
+		if ( stack === this._visibleStack ) {
+			this._showView( data );
+		}
 	}
 
 	/**
 	 * Removes the given view from the stack. If the removed view was visible,
 	 * then the view preceding it in the stack will become visible instead.
-	 * When there is no view in the stack then balloon will hide.
+	 * When there is no view in the stack then next stack will be displayed.
+	 * When there is not more stacks then balloon will hide.
 	 *
 	 * @param {module:ui/view~View} view A view to be removed from the balloon.
 	 */
@@ -157,43 +250,207 @@ export default class ContextualBalloon extends Plugin {
 			throw new CKEditorError( 'contextualballoon-remove-view-not-exist: Cannot remove configuration of not existing view.' );
 		}
 
-		// When visible view is being removed.
-		if ( this.visibleView === view ) {
-			// We need to remove it from the view content.
-			this.view.content.remove( view );
-
-			// And then remove from the stack.
-			this._stack.delete( view );
+		const stack = this._viewToStack.get( view );
 
-			// Next we need to check if there is other view in stack to show.
-			const last = Array.from( this._stack.values() ).pop();
+		if ( this._singleViewMode && this.visibleView === view ) {
+			this._singleViewMode = false;
+		}
 
-			// If it is some other view.
-			if ( last ) {
-				// Just show it.
-				this._show( last );
+		// When visible view will be removed we need to show a preceding view or next stack
+		// if a view is the only view in the stack.
+		if ( this.visibleView === view ) {
+			if ( stack.size === 1 ) {
+				if ( this._idToStack.size > 1 ) {
+					this._showNextStack();
+				} else {
+					this.view.hide();
+					this.visibleView = null;
+					this._rotatorView.hideView();
+				}
 			} else {
-				// Hide the balloon panel.
-				this.view.hide();
+				this._showView( Array.from( stack.values() )[ stack.size - 2 ] );
 			}
+		}
+
+		if ( stack.size === 1 ) {
+			this._idToStack.delete( this._getStackId( stack ) );
+			this._numberOfStacks = this._idToStack.size;
 		} else {
-			// Just remove given view from the stack.
-			this._stack.delete( view );
+			stack.delete( view );
 		}
+
+		this._viewToStack.delete( view );
 	}
 
 	/**
 	 * Updates the position of the balloon using position data of the first visible view in the stack.
-	 * When new position data is given then position data of currently visible panel will be updated.
+	 * When new position data is given then position data of currently visible view will be updated.
 	 *
 	 * @param {module:utils/dom/position~Options} [position] position options.
 	 */
 	updatePosition( position ) {
 		if ( position ) {
-			this._stack.get( this.visibleView ).position = position;
+			this._visibleStack.get( this.visibleView ).position = position;
 		}
 
 		this.view.pin( this._getBalloonPosition() );
+		this._fakePanelsView.updatePosition();
+	}
+
+	/**
+	 * Shows last view from the stack of a given id.
+	 *
+	 * @param {String} id
+	 */
+	showStack( id ) {
+		this.visibleStack = id;
+		const stack = this._idToStack.get( id );
+
+		if ( !stack ) {
+			/**
+			 * Trying to show not existing stack.
+			 *
+			 * @error contextualballoon-showstack-stack-not-exist
+			 */
+			throw new CKEditorError( 'contextualballoon-showstack-stack-not-exist: Cannot show not existing stack.' );
+		}
+
+		if ( this._visibleStack === stack ) {
+			return;
+		}
+
+		this._showView( Array.from( stack.values() ).pop() );
+	}
+
+	/**
+	 * Returns the stack of currently visible view.
+	 *
+	 * @private
+	 * @type {Set}
+	 */
+	get _visibleStack() {
+		return this._viewToStack.get( this.visibleView );
+	}
+
+	/**
+	 * Returns id of given stack.
+	 *
+	 * @private
+	 * @param {Set} stack
+	 * @returns {String}
+	 */
+	_getStackId( stack ) {
+		const entry = Array.from( this._idToStack.entries() ).find( entry => entry[ 1 ] === stack );
+
+		return entry[ 0 ];
+	}
+
+	/**
+	 * Shows last view from the next stack.
+	 *
+	 * @private
+	 */
+	_showNextStack() {
+		const stacks = Array.from( this._idToStack.values() );
+
+		let nextIndex = stacks.indexOf( this._visibleStack ) + 1;
+
+		if ( !stacks[ nextIndex ] ) {
+			nextIndex = 0;
+		}
+
+		this.showStack( this._getStackId( stacks[ nextIndex ] ) );
+	}
+
+	/**
+	 * Shows last view from the previous stack.
+	 *
+	 * @private
+	 */
+	_showPrevStack() {
+		const stacks = Array.from( this._idToStack.values() );
+
+		let nextIndex = stacks.indexOf( this._visibleStack ) - 1;
+
+		if ( !stacks[ nextIndex ] ) {
+			nextIndex = stacks.length - 1;
+		}
+
+		this.showStack( this._getStackId( stacks[ nextIndex ] ) );
+	}
+
+	/**
+	 * Creates a rotator view.
+	 *
+	 * @private
+	 * @returns {module:ui/panel/balloon/contextualballoon~RotatorView}
+	 */
+	_createRotatorView() {
+		const view = new RotatorView( this.editor.locale );
+		const t = this.editor.locale.t;
+
+		this.view.content.add( view );
+
+		// Hide navigation when there is only a one stack & not in single view mode.
+		view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( value, isSingleViewMode ) => {
+			return !isSingleViewMode && value > 1;
+		} );
+
+		// Update balloon position after toggling navigation.
+		view.on( 'change:isNavigationVisible', () => ( this.updatePosition() ), { priority: 'low' } );
+
+		// Update stacks counter value.
+		view.bind( 'counter' ).to( this, 'visibleView', this, '_numberOfStacks', ( visibleView, numberOfStacks ) => {
+			if ( numberOfStacks < 2 ) {
+				return '';
+			}
+
+			const current = Array.from( this._idToStack.values() ).indexOf( this._visibleStack ) + 1;
+
+			return t( '%0 of %1', [ current, numberOfStacks ] );
+		} );
+
+		view.buttonNextView.on( 'execute', () => {
+			// When current view has a focus then move focus to the editable before removing it,
+			// otherwise editor will lost focus.
+			if ( view.focusTracker.isFocused ) {
+				this.editor.editing.view.focus();
+			}
+
+			this._showNextStack();
+		} );
+
+		view.buttonPrevView.on( 'execute', () => {
+			// When current view has a focus then move focus to the editable before removing it,
+			// otherwise editor will lost focus.
+			if ( view.focusTracker.isFocused ) {
+				this.editor.editing.view.focus();
+			}
+
+			this._showPrevStack();
+		} );
+
+		return view;
+	}
+
+	/**
+	 * @returns {module:ui/view~View}
+	 */
+	_createFakePanelsView() {
+		const view = new FakePanelsView( this.editor.locale, this.view );
+
+		view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( number, isSingleViewMode ) => {
+			const showPanels = !isSingleViewMode && number >= 2;
+
+			return showPanels ? Math.min( number - 1, 2 ) : 0;
+		} );
+
+		view.listenTo( this.view, 'change:top', () => view.updatePosition() );
+		view.listenTo( this.view, 'change:left', () => view.updatePosition() );
+
+		this.editor.ui.view.body.add( view );
+
+		return view;
 	}
 
 	/**
@@ -206,12 +463,18 @@ export default class ContextualBalloon extends Plugin {
 	 * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
 	 * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
 	 */
-	_show( { view, balloonClassName = '', withArrow = true } ) {
+	_showView( { view, balloonClassName = '', withArrow = true, singleViewMode = false } ) {
 		this.view.class = balloonClassName;
 		this.view.withArrow = withArrow;
 
-		this.view.content.add( view );
+		this._rotatorView.showView( view );
+		this.visibleView = view;
 		this.view.pin( this._getBalloonPosition() );
+		this._fakePanelsView.updatePosition();
+
+		if ( singleViewMode ) {
+			this._singleViewMode = true;
+		}
 	}
 
 	/**
@@ -222,7 +485,7 @@ export default class ContextualBalloon extends Plugin {
 	 * @returns {module:utils/dom/position~Options}
 	 */
 	_getBalloonPosition() {
-		let position = Array.from( this._stack.values() ).pop().position;
+		let position = Array.from( this._visibleStack.values() ).pop().position;
 
 		// Use the default limiter if none has been specified.
 		if ( position && !position.limiter ) {
@@ -235,3 +498,269 @@ export default class ContextualBalloon extends Plugin {
 		return position;
 	}
 }
+
+/**
+ * Rotator view is a helper class for the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon}.
+ * It is used for displaying last view from the current stack and providing navigation buttons for switching stacks.
+ * See {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon} documentation to learn more.
+ *
+ * @extends module:ui/view~View
+ */
+class RotatorView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const t = locale.t;
+		const bind = this.bindTemplate;
+
+		/**
+		 * Defines whether navigation is visible or not.
+		 *
+		 * @member {Boolean} #isNavigationVisible
+		 */
+		this.set( 'isNavigationVisible', true );
+
+		/**
+		 * Used for checking if view is focused or not.
+		 *
+		 * @type {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * Navigation button for switching stack to the previous one.
+		 *
+		 * @type {module:ui/button/buttonview~ButtonView}
+		 */
+		this.buttonPrevView = this._createButtonView( t( 'Previous' ), prevIcon );
+
+		/**
+		 * Navigation button for switching stack to the next one.
+		 *
+		 * @type {module:ui/button/buttonview~ButtonView}
+		 */
+		this.buttonNextView = this._createButtonView( t( 'Next' ), nextIcon );
+
+		/**
+		 * Collection of the child views which creates rotator content.
+		 *
+		 * @readonly
+		 * @type {module:ui/viewcollection~ViewCollection}
+		 */
+		this.content = this.createCollection();
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck',
+					'ck-balloon-rotator'
+				],
+				'z-index': '-1'
+			},
+			children: [
+				{
+					tag: 'div',
+					attributes: {
+						class: [
+							'ck-balloon-rotator__navigation',
+							bind.to( 'isNavigationVisible', value => value ? '' : 'ck-hidden' )
+						]
+					},
+					children: [
+						this.buttonPrevView,
+						{
+							tag: 'span',
+
+							attributes: {
+								class: [
+									'ck-balloon-rotator__counter'
+								]
+							},
+
+							children: [
+								{
+									text: bind.to( 'counter' )
+								}
+							]
+						},
+						this.buttonNextView,
+					]
+				},
+				{
+					tag: 'div',
+					attributes: {
+						class: 'ck-balloon-rotator__content'
+					},
+					children: this.content
+				}
+			]
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		this.focusTracker.add( this.element );
+	}
+
+	/**
+	 * Shows given view.
+	 *
+	 * @param {module:ui/view~View} view The view to show.
+	 */
+	showView( view ) {
+		this.hideView();
+		this.content.add( view );
+	}
+
+	/**
+	 * Hides currently displayed view.
+	 */
+	hideView() {
+		this.content.clear();
+	}
+
+	/**
+	 * Creates a navigation button view.
+	 *
+	 * @private
+	 * @param {String} label The button's label.
+	 * @param {String} icon The button's icon.
+	 * @returns {module:ui/button/buttonview~ButtonView}
+	 */
+	_createButtonView( label, icon ) {
+		const view = new ButtonView( this.locale );
+
+		view.set( {
+			label,
+			icon,
+			tooltip: true
+		} );
+
+		return view;
+	}
+}
+
+// Displays additional layers under the balloon when multiple stacks are added to the balloon.
+//
+// @private
+// @extends module:ui/view~View
+class FakePanelsView extends View {
+	// @inheritDoc
+	constructor( locale, balloonPanelView ) {
+		super( locale );
+
+		const bind = this.bindTemplate;
+
+		// Fake panels top offset.
+		//
+		// @observable
+		// @member {Number} #top
+		this.set( 'top', 0 );
+
+		// Fake panels left offset.
+		//
+		// @observable
+		// @member {Number} #left
+		this.set( 'left', 0 );
+
+		// Fake panels height.
+		//
+		// @observable
+		// @member {Number} #height
+		this.set( 'height', 0 );
+
+		// Fake panels width.
+		//
+		// @observable
+		// @member {Number} #width
+		this.set( 'width', 0 );
+
+		// Number of rendered fake panels.
+		//
+		// @observable
+		// @member {Number} #numberOfPanels
+		this.set( 'numberOfPanels', 0 );
+
+		// Collection of the child views which creates fake panel content.
+		//
+		// @readonly
+		// @type {module:ui/viewcollection~ViewCollection}
+		this.content = this.createCollection();
+
+		// Context.
+		//
+		// @private
+		// @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
+		this._balloonPanelView = balloonPanelView;
+
+		this.setTemplate( {
+			tag: 'div',
+			attributes: {
+				class: [
+					'ck-fake-panel',
+					bind.to( 'numberOfPanels', number => number ? '' : 'ck-hidden' )
+				],
+				style: {
+					top: bind.to( 'top', toPx ),
+					left: bind.to( 'left', toPx ),
+					width: bind.to( 'width', toPx ),
+					height: bind.to( 'height', toPx )
+				}
+			},
+			children: this.content
+		} );
+
+		this.on( 'change:numberOfPanels', ( evt, name, next, prev ) => {
+			if ( next > prev ) {
+				this._addPanels( next - prev );
+			} else {
+				this._removePanels( prev - next );
+			}
+
+			this.updatePosition();
+		} );
+	}
+
+	// @private
+	// @param {Number} number
+	_addPanels( number ) {
+		while ( number-- ) {
+			const view = new View();
+
+			view.setTemplate( { tag: 'div' } );
+
+			this.content.add( view );
+			this.registerChild( view );
+		}
+	}
+
+	// @private
+	// @param {Number} number
+	_removePanels( number ) {
+		while ( number-- ) {
+			const view = this.content.last;
+
+			this.content.remove( view );
+			this.deregisterChild( view );
+			view.destroy();
+		}
+	}
+
+	// Updates coordinates of fake panels.
+	updatePosition() {
+		if ( this.numberOfPanels ) {
+			const { top, left } = this._balloonPanelView;
+			const { width, height } = new Rect( this._balloonPanelView.element );
+
+			Object.assign( this, { top, left, width, height } );
+		}
+	}
+}

+ 0 - 4
packages/ckeditor5-ui/src/viewcollection.js

@@ -8,9 +8,7 @@
  */
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
  * Collects {@link module:ui/view~View} instances.
@@ -206,8 +204,6 @@ export default class ViewCollection extends Collection {
 	 */
 }
 
-mix( Collection, ObservableMixin );
-
 // Check if all entries of the array are of `String` type.
 //
 // @private

+ 24 - 1
packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.html

@@ -4,7 +4,7 @@
 	<p>Line of text, line of text, line of text, line of text.</p>
 	<p>Line of text, line of text, line of text, line of text.</p>
 	<p>Line of text, line of text, line of text, line of text.</p>
-	<p>Line of text, line of text, line of text, line of text.</p>
+	<p>Line of text, line of [select] text, line of text, line of text.</p>
 	<p>Line of text, line of text, line of text, line of text.</p>
 	<p>Line of text, line of text, line of text, line of text.</p>
 	<p>Line of text, line of text, line of text, line of text.</p>
@@ -20,4 +20,27 @@
 		margin: 5em auto;
 		max-width: 70%;
 	}
+
+	.highlight.yellow {
+		background: hsla(55, 100%, 50%, 0.75);
+	}
+
+	.highlight.blue {
+		background: hsla(249, 100%, 50%, 0.75);
+	}
+
+	.highlight.pink {
+		background: hsla(313, 100%, 50%, 0.75);
+	}
+
+	.highlight.green {
+		background: hsla(114, 100%, 50%, 0.75);
+	}
+
+	.ck p.custom-view {
+		width: 300px;
+		text-align: center;
+		font-size: 14px;
+		padding: 10px 10px 9px;
+	}
 </style>

+ 95 - 3
packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.js

@@ -7,17 +7,109 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Mention from '@ckeditor/ckeditor5-mention/src/mention';
 import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
+import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
+import View from '../../../src/view';
+
+class CustomStackHighlight {
+	static get requires() {
+		return [ ContextualBalloon ];
+	}
+
+	constructor( editor ) {
+		this.editor = editor;
+
+		this._markerToView = new Map();
+	}
+
+	init() {
+		this.editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
+			model: 'highlight',
+			view: data => {
+				const color = data.markerName.split( ':' )[ 2 ];
+
+				return { classes: 'highlight ' + color };
+			}
+		} );
+
+		this.editor.model.document.selection.markers.on( 'add', ( evt, item ) => this._add( item ) );
+		this.editor.model.document.selection.markers.on( 'remove', ( evt, item ) => this._remove( item ) );
+	}
+
+	_add( marker ) {
+		const view = new View();
+
+		view.setTemplate( {
+			tag: 'p',
+			attributes: {
+				class: 'custom-view'
+			},
+			children: [
+				{ text: 'View in separate stack.' }
+			]
+		} );
+
+		this._markerToView.set( marker, view );
+
+		this.editor.plugins.get( ContextualBalloon ).add( {
+			view,
+			stackId: 'custom-' + marker.name.split( ':' )[ 1 ],
+			position: {
+				target: this._getMarkerDomElement( marker )
+			}
+		} );
+	}
+
+	_remove( marker ) {
+		this.editor.plugins.get( ContextualBalloon ).remove( this._markerToView.get( marker ) );
+		this._markerToView.delete( marker );
+	}
+
+	_getMarkerDomElement( marker ) {
+		const editing = this.editor.editing;
+		const viewRange = editing.mapper.toViewRange( marker.getRange() );
+
+		return editing.view.domConverter.viewRangeToDom( viewRange );
+	}
+}
 
-// Finally the editor.
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, BalloonToolbar ],
+		plugins: [ ArticlePluginSet, BalloonToolbar, CustomStackHighlight, Mention ],
 		toolbar: [ 'bold', 'link' ],
-		balloonToolbar: [ 'bold', 'link' ]
+		balloonToolbar: [ 'bold', 'link' ],
+		mention: {
+			feeds: [
+				{
+					marker: '@',
+					feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ]
+				}
+			]
+		}
 	} )
 	.then( editor => {
 		window.editor = editor;
+
+		editor.model.change( writer => {
+			const root = editor.model.document.getRoot();
+
+			[
+				{ id: 1, start: [ 1, 5 ], end: [ 1, 26 ], color: 'yellow' },
+				{ id: 2, start: [ 1, 2 ], end: [ 1, 33 ], color: 'green' },
+				{ id: 3, start: [ 5, 20 ], end: [ 5, 35 ], color: 'blue' },
+				{ id: 4, start: [ 5, 15 ], end: [ 5, 40 ], color: 'pink' },
+				{ id: 5, start: [ 5, 10 ], end: [ 5, 45 ], color: 'yellow' }
+			].forEach( data => {
+				writer.addMarker( `highlight:${ data.id }:${ data.color }`, {
+					range: writer.createRange(
+						writer.createPositionFromPath( root, data.start ),
+						writer.createPositionFromPath( root, data.end )
+					),
+					usingOperation: true
+				} );
+			} );
+		} );
 	} )
 	.catch( err => {
 		console.error( err.stack );

+ 21 - 0
packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.md

@@ -1,4 +1,25 @@
+## Single stack
+
 1. Select some text in the middle of the content - contextual toolbar should show up.
 2. Click link icon in the contextual toolbar - link balloon should open at the position as contextual toolbar.
 3. Close link balloon (Esc press or Cancel button) - contextual toolbar should show up.
 4. Repeat this for backward selection.
+
+## Multiple stacks
+
+1. Select some highlighted text - "View in separate stack." should show up.
+2. Switch stacks by clicking navigation buttons. You should switch between toolbar and custom views.
+
+## Fake panels - min
+
+1. Put the selection before the highlight, start moving selection by right arrow. You should see additional layer under the balloon only when at least 2 stacks are added to the balloon.
+
+## Fake panels - max
+
+1. Select text `[select]` (by non-collapsed selection) from the lower highlight. You should see `1 of 4` status of pagination but only 2 additional layers under the balloon should be visible.
+
+## Force single view - Mention
+
+1. Select text `[select]` (by non-collapsed selection).
+2. Type <kbd>space</kbd> + `@` to open mention panel. You should see mention panel with no layers under the balloon and without any counter.
+3. Move selection around `@` when leaving mention suggestions the balloon should be displayed as in above cases (layers, navigation buttons, etc).

+ 13 - 0
packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js

@@ -192,6 +192,19 @@ describe( 'BalloonPanelView', () => {
 			} ) );
 		} );
 
+		it( 'should parse optimal position offset to int', () => {
+			testUtils.sinon.stub( BalloonPanelView, '_getOptimalPosition' ).returns( {
+				top: 10.345,
+				left: 10.345,
+				name: 'position'
+			} );
+
+			view.attachTo( { target, limiter } );
+
+			expect( view.top ).to.equal( 10 );
+			expect( view.left ).to.equal( 10 );
+		} );
+
 		describe( 'limited by limiter element', () => {
 			beforeEach( () => {
 				// Mock limiter element dimensions.

+ 710 - 30
packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js

@@ -11,11 +11,33 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 /* global document, Event */
 
 describe( 'ContextualBalloon', () => {
-	let editor, editorElement, balloon, viewA, viewB;
+	let editor, editorElement, balloon, viewA, viewB, viewC, viewD;
+
+	testUtils.createSinonSandbox();
+
+	before( () => {
+		addTranslations( 'en', {
+			'Choose heading': '%0 of %1',
+			'Previous': 'Previous',
+			'Next': 'Next'
+		} );
+
+		addTranslations( 'pl', {
+			'%0 of %1': '%0 z %1',
+			'Previous': 'Poprzedni',
+			'Next': 'Następny'
+		} );
+	} );
+
+	after( () => {
+		clearTranslations();
+	} );
 
 	beforeEach( () => {
 		editorElement = document.createElement( 'div' );
@@ -36,6 +58,8 @@ describe( 'ContextualBalloon', () => {
 
 				viewA = new View();
 				viewB = new View();
+				viewC = new View();
+				viewD = new View();
 
 				// Add viewA to the pane and init viewB.
 				balloon.add( {
@@ -65,7 +89,7 @@ describe( 'ContextualBalloon', () => {
 		} );
 	} );
 
-	describe( 'init()', () => {
+	describe( 'constructor()', () => {
 		it( 'should create a plugin instance with properties', () => {
 			expect( balloon.view ).to.instanceof( BalloonPanelView );
 		} );
@@ -161,9 +185,11 @@ describe( 'ContextualBalloon', () => {
 	} );
 
 	describe( 'add()', () => {
-		it( 'should add view to the stack and display in balloon attached using given position options', () => {
-			expect( balloon.view.content.length ).to.equal( 1 );
-			expect( balloon.view.content.get( 0 ) ).to.deep.equal( viewA );
+		it( 'should add view to the `main` stack and display in balloon attached using given position options', () => {
+			const content = balloon.view.content.get( 0 ).content;
+
+			expect( content.length ).to.equal( 1 );
+			expect( content.get( 0 ) ).to.deep.equal( viewA );
 			expect( balloon.view.pin.calledOnce ).to.true;
 			sinon.assert.calledWithMatch( balloon.view.pin.firstCall, {
 				target: 'fake',
@@ -171,6 +197,58 @@ describe( 'ContextualBalloon', () => {
 			} );
 		} );
 
+		it( 'should add view to the custom stack but not display it when other stack is already visible', () => {
+			balloon.add( {
+				view: viewB,
+				stackId: 'second',
+				position: {
+					target: 'fake'
+				}
+			} );
+
+			balloon.add( {
+				view: viewC,
+				stackId: 'second',
+				position: {
+					target: 'fake'
+				}
+			} );
+
+			const content = balloon.view.content.get( 0 ).content;
+
+			expect( content.length ).to.equal( 1 );
+			expect( content.get( 0 ) ).to.deep.equal( viewA );
+			expect( balloon.hasView( viewB ) );
+			expect( balloon.hasView( viewC ) );
+		} );
+
+		it( 'should add multiple views to he stack and display last one', () => {
+			balloon.add( {
+				view: viewB,
+				position: {
+					target: 'fake',
+					limiter: balloon.positionLimiter
+				}
+			} );
+
+			const content = balloon.view.content.get( 0 ).content;
+
+			expect( content.length ).to.equal( 1 );
+			expect( content.get( 0 ) ).to.deep.equal( viewB );
+		} );
+
+		it( 'should throw an error when try to add the same view more than once', () => {
+			expect( () => {
+				balloon.add( {
+					view: viewA,
+					position: {
+						target: 'fake',
+						limiter: balloon.positionLimiter
+					}
+				} );
+			} ).to.throw( CKEditorError, /^contextualballoon-add-view-exist/ );
+		} );
+
 		it( 'should use a provided limiter instead of #positionLimiter', () => {
 			balloon.remove( viewA );
 			balloon.view.pin.resetHistory();
@@ -230,31 +308,6 @@ describe( 'ContextualBalloon', () => {
 			sinon.assert.calledOnce( balloon.view.pin );
 		} );
 
-		it( 'should throw an error when try to add the same view more than once', () => {
-			expect( () => {
-				balloon.add( {
-					view: viewA,
-					position: {
-						target: 'fake',
-						limiter: balloon.positionLimiter
-					}
-				} );
-			} ).to.throw( CKEditorError, /^contextualballoon-add-view-exist/ );
-		} );
-
-		it( 'should add multiple views to he stack and display last one', () => {
-			balloon.add( {
-				view: viewB,
-				position: {
-					target: 'fake',
-					limiter: balloon.positionLimiter
-				}
-			} );
-
-			expect( balloon.view.content.length ).to.equal( 1 );
-			expect( balloon.view.content.get( 0 ) ).to.deep.equal( viewB );
-		} );
-
 		it( 'should use the position of the last view in the stack', () => {
 			balloon.add( {
 				view: viewB,
@@ -363,6 +416,53 @@ describe( 'ContextualBalloon', () => {
 			balloon.remove( viewA );
 			expect( balloon.visibleView ).to.null;
 		} );
+
+		it( 'should be observable', () => {
+			const spy = sinon.spy();
+
+			balloon.on( 'change:visibleView', spy );
+
+			balloon.add( { view: viewB } );
+
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWith( spy, sinon.match.any, 'visibleView', viewB, viewA );
+		} );
+	} );
+
+	describe( 'showStack()', () => {
+		it( 'should hide current view and display last view from the given stack', () => {
+			balloon.add( {
+				stackId: 'second',
+				view: viewB
+			} );
+
+			balloon.add( {
+				stackId: 'second',
+				view: viewC
+			} );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+
+			balloon.showStack( 'second' );
+
+			expect( balloon.visibleView ).to.equal( viewC );
+
+			balloon.showStack( 'main' );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+		} );
+
+		it( 'should do nothing when given stack is already visible', () => {
+			expect( () => {
+				balloon.showStack( 'main' );
+			} ).to.not.throw();
+		} );
+
+		it( 'should throw an error when there is no stack of given id', () => {
+			expect( () => {
+				balloon.showStack( 'second' );
+			} ).to.throw( CKEditorError, 'contextualballoon-showstack-stack-not-exist: Cannot show not existing stack.' );
+		} );
 	} );
 
 	describe( 'remove()', () => {
@@ -375,6 +475,53 @@ describe( 'ContextualBalloon', () => {
 			expect( balloon.view.isVisible ).to.false;
 		} );
 
+		it( 'should remove given view from not displayed stack', () => {
+			balloon.add( {
+				stackId: 'second',
+				view: viewB
+			} );
+
+			balloon.add( {
+				stackId: 'second',
+				view: viewC
+			} );
+
+			balloon.remove( viewB );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+			expect( () => {
+				balloon.showStack( 'second' );
+			} ).to.not.throw();
+		} );
+
+		it( 'should remove not displayed stack if a removed view was the only view in this stack', () => {
+			balloon.add( {
+				stackId: 'second',
+				view: viewB
+			} );
+
+			balloon.remove( viewB );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+			expect( () => {
+				balloon.showStack( 'second' );
+			} ).to.throw();
+		} );
+
+		it( 'should switch stack to the next one when removed view was the last one in the visible stack', () => {
+			balloon.add( {
+				stackId: 'second',
+				view: viewB
+			} );
+
+			balloon.remove( viewA );
+
+			expect( balloon.visibleView ).to.equal( viewB );
+			expect( () => {
+				balloon.showStack( 'main' );
+			} ).to.throw();
+		} );
+
 		it( 'should remove given view and set preceding in the stack as visible when removed view was visible', () => {
 			balloon.add( {
 				view: viewB,
@@ -403,6 +550,53 @@ describe( 'ContextualBalloon', () => {
 			expect( balloon.visibleView ).to.equal( viewB );
 		} );
 
+		it( 'should remove given view from a not currently visible stack', () => {
+			balloon.add( {
+				view: viewB,
+				stackId: 'second',
+				position: {
+					target: 'fake'
+				}
+			} );
+
+			balloon.add( {
+				view: viewC,
+				stackId: 'second',
+				position: {
+					target: 'fake'
+				}
+			} );
+
+			balloon.remove( viewB );
+
+			expect( balloon.hasView( viewB ) ).to.false;
+			expect( balloon.hasView( viewC ) ).to.true;
+
+			// Does not throw, so the stack is there.
+			expect( () => {
+				balloon.showStack( 'second' );
+			} ).to.not.throw();
+		} );
+
+		it( 'should remove not displayed stack when removied view was the last one in the stack', () => {
+			balloon.add( {
+				view: viewB,
+				stackId: 'second',
+				position: {
+					target: 'fake'
+				}
+			} );
+
+			balloon.remove( viewB );
+
+			expect( balloon.hasView( viewB ) ).to.false;
+
+			// Does throw, so the stack is not there.
+			expect( () => {
+				balloon.showStack( 'second' );
+			} ).to.throw();
+		} );
+
 		it( 'should throw an error when there is no given view in the stack', () => {
 			expect( () => {
 				balloon.remove( viewB );
@@ -533,4 +727,490 @@ describe( 'ContextualBalloon', () => {
 			expect( editor.ui.view.body.getIndex( balloon.view ) ).to.not.equal( -1 );
 		} );
 	} );
+
+	describe( 'rotator view', () => {
+		let rotatorView;
+
+		beforeEach( () => {
+			rotatorView = balloon.view.content.get( 0 );
+		} );
+
+		it( 'should display navigation when there is more than one stack', () => {
+			const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+			expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.equal( true );
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.equal( false );
+		} );
+
+		it( 'should display counter', () => {
+			const counterElement = rotatorView.element.querySelector( '.ck-balloon-rotator__counter' );
+
+			expect( counterElement.textContent ).to.equal( '' );
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			expect( counterElement.textContent ).to.equal( '1 of 2' );
+
+			balloon.showStack( 'second' );
+
+			expect( counterElement.textContent ).to.equal( '2 of 2' );
+		} );
+
+		it( 'should switch stack to the next one after clicking next button', () => {
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			balloon.add( {
+				view: viewC,
+				stackId: 'third'
+			} );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+
+			rotatorView.buttonNextView.fire( 'execute' );
+
+			expect( balloon.visibleView ).to.equal( viewB );
+
+			rotatorView.buttonNextView.fire( 'execute' );
+
+			expect( balloon.visibleView ).to.equal( viewC );
+
+			rotatorView.buttonNextView.fire( 'execute' );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+		} );
+
+		it( 'should not move focus to the editable when switching not focused view to the next one', () => {
+			const editableFocusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			rotatorView.buttonNextView.fire( 'execute' );
+
+			sinon.assert.notCalled( editableFocusSpy );
+		} );
+
+		it( 'should move focus to the editable when switching focused view to the next one', () => {
+			const editableFocusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			rotatorView.focusTracker.isFocused = true;
+
+			rotatorView.buttonNextView.fire( 'execute' );
+
+			sinon.assert.calledOnce( editableFocusSpy );
+		} );
+
+		it( 'should switch stack to the prev one after clicking prev button', () => {
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			balloon.add( {
+				view: viewC,
+				stackId: 'third'
+			} );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+
+			rotatorView.buttonPrevView.fire( 'execute' );
+
+			expect( balloon.visibleView ).to.equal( viewC );
+
+			rotatorView.buttonPrevView.fire( 'execute' );
+
+			expect( balloon.visibleView ).to.equal( viewB );
+
+			rotatorView.buttonPrevView.fire( 'execute' );
+
+			expect( balloon.visibleView ).to.equal( viewA );
+		} );
+
+		it( 'should not move focus to the editable when switching not focused view to the prev one', () => {
+			const editableFocusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			rotatorView.buttonPrevView.fire( 'execute' );
+
+			sinon.assert.notCalled( editableFocusSpy );
+		} );
+
+		it( 'should move focus to the editable when switching focused view to the prev one', () => {
+			const editableFocusSpy = sinon.spy( editor.editing.view, 'focus' );
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			rotatorView.focusTracker.isFocused = true;
+
+			rotatorView.buttonPrevView.fire( 'execute' );
+
+			sinon.assert.calledOnce( editableFocusSpy );
+		} );
+
+		it( 'should add hidden view with fake panels to editor body collection', () => {
+			const fakePanelsView = editor.ui.view.body.last;
+
+			expect( fakePanelsView.element.classList.contains( 'ck-fake-panel' ) ).to.equal( true );
+			expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( true );
+			expect( fakePanelsView.element.childElementCount ).to.equal( 0 );
+		} );
+
+		it( 'should show fake panels when more than one stack is added to the balloon (max to 2 panels)', () => {
+			const fakePanelsView = editor.ui.view.body.last;
+			const viewD = new View();
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+			expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
+
+			balloon.add( {
+				view: viewC,
+				stackId: 'third'
+			} );
+
+			expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+			expect( fakePanelsView.element.childElementCount ).to.equal( 2 );
+
+			balloon.add( {
+				view: viewD,
+				stackId: 'fourth'
+			} );
+
+			expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+			expect( fakePanelsView.element.childElementCount ).to.equal( 2 );
+
+			balloon.remove( viewD );
+
+			expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+			expect( fakePanelsView.element.childElementCount ).to.equal( 2 );
+
+			balloon.remove( viewC );
+
+			expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+			expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
+
+			balloon.remove( viewB );
+
+			expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( true );
+			expect( fakePanelsView.element.childElementCount ).to.equal( 0 );
+		} );
+
+		it( 'should keep position of fake panels up to date with balloon position when panels are visible', () => {
+			const fakePanelsView = editor.ui.view.body.last;
+
+			let width = 30;
+			let height = 40;
+
+			balloon.view.top = 10;
+			balloon.view.left = 20;
+
+			sinon.stub( balloon.view.element, 'getBoundingClientRect' ).callsFake( () => ( { width, height } ) );
+
+			balloon.add( {
+				view: viewB,
+				stackId: 'second'
+			} );
+
+			expect( fakePanelsView.element.style.top ).to.equal( '10px' );
+			expect( fakePanelsView.element.style.left ).to.equal( '20px' );
+			expect( fakePanelsView.element.style.width ).to.equal( '30px' );
+			expect( fakePanelsView.element.style.height ).to.equal( '40px' );
+
+			balloon.view.top = 15;
+			balloon.view.left = 25;
+			width = 35;
+			height = 45;
+
+			balloon.add( {
+				view: viewC,
+				stackId: 'third'
+			} );
+
+			expect( fakePanelsView.element.style.top ).to.equal( '15px' );
+			expect( fakePanelsView.element.style.left ).to.equal( '25px' );
+			expect( fakePanelsView.element.style.width ).to.equal( '35px' );
+			expect( fakePanelsView.element.style.height ).to.equal( '45px' );
+
+			balloon.view.top = 10;
+			balloon.view.left = 20;
+			width = 30;
+			height = 40;
+
+			balloon.updatePosition();
+
+			expect( fakePanelsView.element.style.top ).to.equal( '10px' );
+			expect( fakePanelsView.element.style.left ).to.equal( '20px' );
+			expect( fakePanelsView.element.style.width ).to.equal( '30px' );
+			expect( fakePanelsView.element.style.height ).to.equal( '40px' );
+
+			// Hide fake panels by removing additional stacks.
+			balloon.remove( viewC );
+			balloon.remove( viewB );
+
+			balloon.view.top = 15;
+			balloon.view.left = 25;
+			width = 35;
+			height = 45;
+
+			balloon.updatePosition();
+
+			// Old values because fake panels are hidden.
+			expect( fakePanelsView.element.style.top ).to.equal( '10px' );
+			expect( fakePanelsView.element.style.left ).to.equal( '20px' );
+			expect( fakePanelsView.element.style.width ).to.equal( '30px' );
+			expect( fakePanelsView.element.style.height ).to.equal( '40px' );
+		} );
+
+		it( 'should translate the views', () => {
+			// Cleanup the editor created by contextual balloon suite beforeEach.
+			return editor.destroy()
+				.then( () => {
+					editorElement.remove();
+
+					// Setup localized editor for language tests.
+					editorElement = document.createElement( 'div' );
+					document.body.appendChild( editorElement );
+
+					return ClassicTestEditor
+						.create( editorElement, {
+							plugins: [ Paragraph, ContextualBalloon ],
+							language: 'pl'
+						} );
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					balloon = editor.plugins.get( ContextualBalloon );
+					// We don't need to execute BalloonPanel pin and attachTo methods
+					// it's enough to check if was called with the proper data.
+					sinon.stub( balloon.view, 'attachTo' ).returns( {} );
+					sinon.stub( balloon.view, 'pin' ).returns( {} );
+
+					balloon.add( {
+						view: new View()
+					} );
+
+					balloon.add( {
+						view: new View(),
+						stackId: 'second'
+					} );
+
+					const rotatorView = balloon.view.content.get( 0 );
+					const counterElement = rotatorView.element.querySelector( '.ck-balloon-rotator__counter' );
+
+					expect( counterElement.textContent ).to.equal( '1 z 2' );
+					expect( rotatorView.buttonPrevView.labelView.element.textContent ).to.equal( 'Poprzedni' );
+					expect( rotatorView.buttonNextView.labelView.element.textContent ).to.equal( 'Następny' );
+				} );
+		} );
+
+		describe( 'singleViewMode', () => {
+			it( 'should not display navigation when there is more than one stack', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+			} );
+
+			it( 'should hide display navigation after adding view', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+			} );
+
+			it( 'should display navigation after removing a view', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.remove( viewC );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+			} );
+
+			it( 'should not display navigation after removing a view if there is still some view with singleViewMode', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				balloon.add( {
+					view: viewD,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.remove( viewD );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.remove( viewC );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+			} );
+
+			it( 'should not show fake panels when more than one stack is added to the balloon (max to 2 panels)', () => {
+				const fakePanelsView = editor.ui.view.body.last;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+				expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.be.true;
+				expect( fakePanelsView.element.childElementCount ).to.equal( 0 );
+
+				balloon.remove( viewC );
+
+				expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+				expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
+
+				balloon.remove( viewB );
+			} );
+
+			it( 'should switch visible view when adding a view to new stack', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewA );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewC );
+
+				const viewD = new View();
+
+				balloon.add( {
+					view: viewD,
+					stackId: 'fifth',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewD );
+			} );
+
+			it( 'should switch visible view when adding a view to the same stack', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewA );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewC );
+
+				const viewD = new View();
+
+				balloon.add( {
+					view: viewD,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewD );
+			} );
+		} );
+	} );
 } );

+ 17 - 0
packages/ckeditor5-ui/theme/components/panel/balloonrotator.css

@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck .ck-balloon-rotator__navigation {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
+/* Buttons inside a toolbar should be centered when rotator bar is wider.
+ * See: https://github.com/ckeditor/ckeditor5-ui/issues/495
+ */
+.ck .ck-balloon-rotator__content .ck-toolbar {
+	justify-content: center;
+}

+ 23 - 0
packages/ckeditor5-ui/theme/components/panel/fakepanel.css

@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+.ck .ck-fake-panel {
+	position: absolute;
+
+	/* Fake panels should be placed under main balloon content. */
+	z-index: calc(var(--ck-z-modal) - 1);
+}
+
+.ck .ck-fake-panel div {
+	position: absolute;
+}
+
+.ck .ck-fake-panel div:nth-child( 1 ) {
+	z-index: 2;
+}
+
+.ck .ck-fake-panel div:nth-child( 2 ) {
+	z-index: 1;
+}

+ 1 - 0
packages/ckeditor5-ui/theme/icons/next-arrow.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.537 14.813a.888.888 0 1 1-1.254-1.255L10.84 10 7.283 6.442a.888.888 0 1 1 1.254-1.255L12.74 9.39a.888.888 0 0 1-.16 1.382l-4.043 4.042z"/></svg>

+ 1 - 0
packages/ckeditor5-ui/theme/icons/previous-arrow.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11.463 5.187a.888.888 0 1 1 1.254 1.255L9.16 10l3.557 3.557a.888.888 0 1 1-1.254 1.255L7.26 10.61a.888.888 0 0 1 .16-1.382l4.043-4.042z" /></svg>