Explorar o código

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

Release: v1.0.0-beta.1.
Piotrek Koszuliński %!s(int64=7) %!d(string=hai) anos
pai
achega
fa61874679
Modificáronse 61 ficheiros con 895 adicións e 561 borrados
  1. 1 1
      packages/ckeditor5-undo/.eslintrc.js
  2. 15 11
      packages/ckeditor5-undo/.travis.yml
  3. 16 0
      packages/ckeditor5-undo/CHANGELOG.md
  4. 1 1
      packages/ckeditor5-undo/LICENSE.md
  5. 3 1
      packages/ckeditor5-undo/README.md
  6. 1 1
      packages/ckeditor5-undo/lang/translations/ast.po
  7. 1 1
      packages/ckeditor5-undo/lang/translations/da.po
  8. 1 1
      packages/ckeditor5-undo/lang/translations/de.po
  9. 1 1
      packages/ckeditor5-undo/lang/translations/el.po
  10. 1 1
      packages/ckeditor5-undo/lang/translations/en-au.po
  11. 1 1
      packages/ckeditor5-undo/lang/translations/en.po
  12. 1 1
      packages/ckeditor5-undo/lang/translations/eo.po
  13. 14 0
      packages/ckeditor5-undo/lang/translations/es.po
  14. 14 0
      packages/ckeditor5-undo/lang/translations/et.po
  15. 14 0
      packages/ckeditor5-undo/lang/translations/eu.po
  16. 1 1
      packages/ckeditor5-undo/lang/translations/fi.po
  17. 1 1
      packages/ckeditor5-undo/lang/translations/fr.po
  18. 1 1
      packages/ckeditor5-undo/lang/translations/gl.po
  19. 1 1
      packages/ckeditor5-undo/lang/translations/hr.po
  20. 1 1
      packages/ckeditor5-undo/lang/translations/hu.po
  21. 1 1
      packages/ckeditor5-undo/lang/translations/it.po
  22. 1 1
      packages/ckeditor5-undo/lang/translations/ja.po
  23. 1 1
      packages/ckeditor5-undo/lang/translations/km.po
  24. 1 1
      packages/ckeditor5-undo/lang/translations/kn.po
  25. 1 1
      packages/ckeditor5-undo/lang/translations/ko.po
  26. 1 1
      packages/ckeditor5-undo/lang/translations/ku.po
  27. 1 1
      packages/ckeditor5-undo/lang/translations/nb.po
  28. 14 0
      packages/ckeditor5-undo/lang/translations/nl.po
  29. 1 1
      packages/ckeditor5-undo/lang/translations/pl.po
  30. 1 1
      packages/ckeditor5-undo/lang/translations/pt-br.po
  31. 1 1
      packages/ckeditor5-undo/lang/translations/pt.po
  32. 1 1
      packages/ckeditor5-undo/lang/translations/ro.po
  33. 1 1
      packages/ckeditor5-undo/lang/translations/ru.po
  34. 14 0
      packages/ckeditor5-undo/lang/translations/si.po
  35. 1 1
      packages/ckeditor5-undo/lang/translations/sk.po
  36. 14 0
      packages/ckeditor5-undo/lang/translations/sq.po
  37. 1 1
      packages/ckeditor5-undo/lang/translations/sv.po
  38. 1 1
      packages/ckeditor5-undo/lang/translations/tr.po
  39. 14 0
      packages/ckeditor5-undo/lang/translations/tt.po
  40. 14 0
      packages/ckeditor5-undo/lang/translations/ug.po
  41. 1 1
      packages/ckeditor5-undo/lang/translations/uk.po
  42. 1 1
      packages/ckeditor5-undo/lang/translations/zh-cn.po
  43. 1 1
      packages/ckeditor5-undo/lang/translations/zh.po
  44. 15 15
      packages/ckeditor5-undo/package.json
  45. 38 28
      packages/ckeditor5-undo/src/basecommand.js
  46. 4 4
      packages/ckeditor5-undo/src/redocommand.js
  47. 8 54
      packages/ckeditor5-undo/src/undo.js
  48. 4 4
      packages/ckeditor5-undo/src/undocommand.js
  49. 30 11
      packages/ckeditor5-undo/src/undoediting.js
  50. 63 0
      packages/ckeditor5-undo/src/undoui.js
  51. 5 6
      packages/ckeditor5-undo/tests/basecommand.js
  52. 2 2
      packages/ckeditor5-undo/tests/manual/undo.js
  53. 48 31
      packages/ckeditor5-undo/tests/redocommand.js
  54. 7 126
      packages/ckeditor5-undo/tests/undo.js
  55. 94 42
      packages/ckeditor5-undo/tests/undocommand.js
  56. 160 115
      packages/ckeditor5-undo/tests/undoediting-integration.js
  57. 154 0
      packages/ckeditor5-undo/tests/undoediting.js
  58. 0 77
      packages/ckeditor5-undo/tests/undoengine.js
  59. 83 0
      packages/ckeditor5-undo/tests/undoui.js
  60. 1 1
      packages/ckeditor5-undo/theme/icons/redo.svg
  61. 1 1
      packages/ckeditor5-undo/theme/icons/undo.svg

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

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

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

@@ -1,24 +1,28 @@
 sudo: required
 dist: trusty
 addons:
+  firefox: "latest"
   apt:
     sources:
-      - google-chrome
+    - google-chrome
     packages:
-      - google-chrome-stable
+    - google-chrome-stable
 language: node_js
 node_js:
-  - "6"
+- '6'
 cache:
-  - node_modules
+- node_modules
 before_install:
-  - export DISPLAY=:99.0
-  - sh -e /etc/init.d/xvfb start
+- export DISPLAY=:99.0
+- sh -e /etc/init.d/xvfb start
 install:
-  - npm install @ckeditor/ckeditor5-dev-tests
-  - ckeditor5-dev-tests-install-dependencies
+- npm install @ckeditor/ckeditor5-dev-tests
+- ckeditor5-dev-tests-install-dependencies
 script:
-  - ckeditor5-dev-tests-travis
+- ckeditor5-dev-tests-travis
 after_success:
-  - codeclimate-test-reporter < coverage/lcov.info
-  - ckeditor5-dev-tests-save-revision
+- ckeditor5-dev-tests-save-revision
+env:
+  global:
+  - secure: eM+m5vGWUkXz0p2SFQd9XflKBhH8BUrkY8PzQ5wYBWG6irB53k/cTdBcVMeBQR6fe+pN/Rha7VBQOTi1fc9xNbdcAU0iN9NKllEV+MJheGTSD5wkTuHgflNatdste8oD1OUNDeFsAFYpRdN6vO7BSnbiDI5jEOusbAvS/uGE8RLT6O0f6Vfb2fVBxYE43Zlvq8+I7z7QhDw1Gu83cBsSxy3wiW7KlWrLY+hppSgQ4eAopZm/f+aQ+YkTJTjPerQ5Dr1qIOIEgzPSCGO3VaaGIXULq3rxHkx4NaQGfoNe1YNIuuCh5Bt0rPuIN+GlNYB68suT9U0zJQ/Cua2Rvy5h0G0hlIrBLyEJHzBar7YcqoPc+lqWcgSJfYYgnQXzTvpcxRFgwHbDSGhERRuoAf6bJgKprt/pDoavAWogLd0nxSYF3aSb6rcME9CdBlUwqh/iPvY+sOJEWHLCasuSXB2ZbaaF9ltHDbO+p0u1OpjnzlpmA3mG7g53q7TCP6yiiiQ5bBorViR75Y/xNkhjs4wbNvpgi2vdVbPxU0wmcZMWcMblchGKfK5X+HieIgaBuja+P6NWAZaleKr3o0dD3eY99Mq38ivn/LTaXr1k+SVDZLqBauJ4a4+oy9LmxqNwVgiRuhWLmDnOOyFfBG5OznCfzCBfVIo9tdsuSWf8STVo+WU=
+  - secure: NheA8ZyyBCJ9j7A3n7GlmCBjzNQkMYRXAAnbgHKD7glAON0MIr79Fr/xQJfDp3cnqPpQsXbQAY/C2gTakSwBvaRADCog3kfdBMIqgAx4jJnXOVJiqXiau3eC3dR7DpVpdcRtuGbpNtCBs5SU9RRWmEBhF2cWxEI/S+wZjlDgrdiq/JVI5BAYj4J+Qi01S3x9Rbfetwo+TtY/E4KbSk6orRU+CSpRHeCFhLw2B6BsLzKec2Dsqn6DscS/yRm1t7Yzpmp0tl3i9+FLdbhZzJdeExY9A4tp21y3rR45dCHrXP5Uk1mVAbyW3XjMStrQ6peC9WuvUhcemUDEdALZjH5Ej1DYdPYYqfXeCEx9FFmy16iUY9QrKy78avmKd35QEBJObgfOEyU5OH8RxHKu5pVDnws7NSJcoN9BPs5678jHysjPyFyRdJRAEKfqeEr2z3oVCHqNBmvxh54Y+L+ecknqyvPTfoM2FzVtJMSawERvTuckYa8uOH8ls2nf2NN2VCrPhWobWh6ZIT7LUlGp4St6tcVH2zf6XVAGmcwDvN/BBQUJm5gEvcSx3h5SxyCP8dbrzZIo/GFcKUfJrtJxnDpkjfXS0WYV5jc+zBk1mCAqo2d7tTwVKF5zK3Y004f5WCF6yQQ53Vpkcfh2CE9xBSD3LyeDRNYeS3S5v1yd0N2M0bc=

+ 16 - 0
packages/ckeditor5-undo/CHANGELOG.md

@@ -1,6 +1,22 @@
 Changelog
 =========
 
+## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-undo/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
+
+### Features
+
+* Updated icons for compatibility with the refreshed Lark theme. Minor adjustment in toolbar configuration (see [ckeditor/ckeditor5#645](https://github.com/ckeditor/ckeditor5/issues/645)). ([254cd89](https://github.com/ckeditor/ckeditor5-undo/commit/254cd89))
+
+### Bug fixes
+
+* Do not register batches which have only non-document operations. Closes [#79](https://github.com/ckeditor/ckeditor5-undo/issues/79). Closes [ckeditor/ckeditor5#781](https://github.com/ckeditor/ckeditor5/issues/781). ([60ac1ab](https://github.com/ckeditor/ckeditor5-undo/commit/60ac1ab))
+
+### Other changes
+
+* Aligned feature class naming to the new scheme. ([d4f0cc2](https://github.com/ckeditor/ckeditor5-undo/commit/d4f0cc2))
+* Updated translations. ([33171d5](https://github.com/ckeditor/ckeditor5-undo/commit/33171d5))
+
+
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-undo/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 
 ### Other changes

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

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

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

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

+ 1 - 1
packages/ckeditor5-undo/lang/translations/ast.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ast\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/da.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: da\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/de.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: de\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/el.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: el\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/en-au.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: en_AU\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/en.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: \n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/eo.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: eo\n"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/es.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: es\n"
+"Language-Team: Spanish (https://www.transifex.com/ckeditor/teams/11143/es/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr "Deshacer"
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "Rehacer"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/et.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: et\n"
+"Language-Team: Estonian (https://www.transifex.com/ckeditor/teams/11143/et/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr "Samm tagasi"
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "Samm edasi"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/eu.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: eu\n"
+"Language-Team: Basque (https://www.transifex.com/ckeditor/teams/11143/eu/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr "Desegin"
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "Berregin"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/fi.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: fi\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/fr.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: fr\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/gl.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: gl\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/hr.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: hr\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/hu.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: hu\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/it.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: it\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/ja.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ja\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/km.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: km\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/kn.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: kn\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/ko.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ko\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/ku.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ku\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/nb.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: nb\n"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/nl.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: nl\n"
+"Language-Team: Dutch (https://www.transifex.com/ckeditor/teams/11143/nl/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr "Ongedaan maken"
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "Opnieuw"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/pl.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: pl\n"

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

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: pt_BR\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/pt.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: pt\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/ro.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ro\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/ru.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: ru\n"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/si.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: si_LK\n"
+"Language-Team: Sinhala (Sri Lanka) (https://www.transifex.com/ckeditor/teams/11143/si_LK/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr "අහෝසි කරන්න"
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "නැවත කරන්න"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/sk.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: sk\n"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/sq.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: sq\n"
+"Language-Team: Albanian (https://www.transifex.com/ckeditor/teams/11143/sq/)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr "Rikthe"
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "Ribëje"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/sv.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: sv\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/tr.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: tr\n"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/tt.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: tt\n"
+"Language-Team: Tatar (https://www.transifex.com/ckeditor/teams/11143/tt/)\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr ""
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "Кабатла"

+ 14 - 0
packages/ckeditor5-undo/lang/translations/ug.po

@@ -0,0 +1,14 @@
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+msgid ""
+msgstr ""
+"Language: ug\n"
+"Language-Team: Uighur (https://www.transifex.com/ckeditor/teams/11143/ug/)\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgctxt "Toolbar button tooltip for the Undo feature."
+msgid "Undo"
+msgstr "قالدۇرۇش"
+
+msgctxt "Toolbar button tooltip for the Redo feature."
+msgid "Redo"
+msgstr "قايتا قىلىش"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/uk.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: uk\n"

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

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: zh_CN\n"

+ 1 - 1
packages/ckeditor5-undo/lang/translations/zh.po

@@ -1,4 +1,4 @@
-# Copyright (c) Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+# Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
 msgid ""
 msgstr ""
 "Language: zh_TW\n"

+ 15 - 15
packages/ckeditor5-undo/package.json

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

+ 38 - 28
packages/ckeditor5-undo/src/basecommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,6 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
 
 /**
  * Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}.
@@ -56,7 +57,7 @@ export default class BaseCommand extends Command {
 	 * @param {module:engine/model/batch~Batch} batch The batch to add.
 	 */
 	addBatch( batch ) {
-		const docSelection = this.editor.document.selection;
+		const docSelection = this.editor.model.document.selection;
 
 		const selection = {
 			ranges: docSelection.hasOwnRange ? Array.from( docSelection.getRanges() ) : [],
@@ -84,7 +85,8 @@ export default class BaseCommand extends Command {
 	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltas Deltas which has been applied since selection has been stored.
 	 */
 	_restoreSelection( ranges, isBackward, deltas ) {
-		const document = this.editor.document;
+		const model = this.editor.model;
+		const document = model.document;
 
 		// This will keep the transformed selection ranges.
 		const selectionRanges = [];
@@ -109,7 +111,9 @@ export default class BaseCommand extends Command {
 
 		// `selectionRanges` may be empty if all ranges ended up in graveyard. If that is the case, do not restore selection.
 		if ( selectionRanges.length ) {
-			document.selection.setRanges( selectionRanges, isBackward );
+			model.change( writer => {
+				writer.setSelection( selectionRanges, { backward: isBackward } );
+			} );
 		}
 	}
 
@@ -121,10 +125,11 @@ export default class BaseCommand extends Command {
 	 * @param {module:engine/model/batch~Batch} batchToUndo The batch to be undone.
 	 */
 	_undo( batchToUndo ) {
-		const document = this.editor.document;
+		const model = this.editor.model;
+		const document = model.document;
 
 		// All changes done by the command execution will be saved as one batch.
-		const undoingBatch = document.batch();
+		const undoingBatch = new Batch();
 		this._createdBatches.add( undoingBatch );
 
 		const deltasToUndo = batchToUndo.deltas.slice();
@@ -133,29 +138,34 @@ export default class BaseCommand extends Command {
 		// We will process each delta from `batchToUndo`, in reverse order. If there were deltas A, B and C in undone batch,
 		// we need to revert them in reverse order, so first C' (reversed C), then B', then A'.
 		for ( const deltaToUndo of deltasToUndo ) {
-			// Keep in mind that transformation algorithms return arrays. That's because the transformation might result in multiple
-			// deltas, so we need arrays to handle them. To simplify algorithms, it is better to always operate on arrays.
-			const nextBaseVersion = deltaToUndo.baseVersion + deltaToUndo.operations.length;
-
-			// Reverse delta from the history.
-			const historyDeltas = Array.from( document.history.getDeltas( nextBaseVersion ) );
-			const transformedSets = document.transformDeltas( [ deltaToUndo.getReversed() ], historyDeltas, true );
-			const reversedDeltas = transformedSets.deltasA;
-
-			// After reversed delta has been transformed by all history deltas, apply it.
-			for ( const delta of reversedDeltas ) {
-				// Fix base version.
-				delta.baseVersion = document.version;
-
-				// Before applying, add the delta to the `undoingBatch`.
-				undoingBatch.addDelta( delta );
-
-				// Now, apply all operations of the delta.
-				for ( const operation of delta.operations ) {
-					document.applyOperation( operation );
+			// For now let's skip deltas with operation applied on detached document.
+			// We assumed that there is no deltas with mixed (document and document fragment) operations
+			// so we can skip entire delta.
+			if ( deltaToUndo.operations.some( op => op.isDocumentOperation ) ) {
+				// Keep in mind that transformation algorithms return arrays. That's because the transformation might result in multiple
+				// deltas, so we need arrays to handle them. To simplify algorithms, it is better to always operate on arrays.
+				const nextBaseVersion = deltaToUndo.baseVersion + deltaToUndo.operations.length;
+
+				// Reverse delta from the history.
+				const historyDeltas = Array.from( document.history.getDeltas( nextBaseVersion ) );
+				const transformedSets = model.transformDeltas( [ deltaToUndo.getReversed() ], historyDeltas, true );
+				const reversedDeltas = transformedSets.deltasA;
+
+				// After reversed delta has been transformed by all history deltas, apply it.
+				for ( const delta of reversedDeltas ) {
+					// Fix base version.
+					delta.baseVersion = document.version;
+
+					// Before applying, add the delta to the `undoingBatch`.
+					undoingBatch.addDelta( delta );
+
+					// Now, apply all operations of the delta.
+					for ( const operation of delta.operations ) {
+						model.applyOperation( operation );
+					}
+
+					document.history.setDeltaAsUndone( deltaToUndo, delta );
 				}
-
-				document.history.setDeltaAsUndone( deltaToUndo, delta );
 			}
 		}
 

+ 4 - 4
packages/ckeditor5-undo/src/redocommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -31,12 +31,12 @@ export default class RedoCommand extends BaseCommand {
 	execute() {
 		const item = this._stack.pop();
 
-		// All changes have to be done in one `enqueueChanges` callback so other listeners will not
+		// All changes have to be done in one `enqueueChange` callback so other listeners will not
 		// step between consecutive deltas, or won't do changes to the document before selection is properly restored.
-		this.editor.document.enqueueChanges( () => {
+		this.editor.model.enqueueChange( () => {
 			const lastDelta = item.batch.deltas[ item.batch.deltas.length - 1 ];
 			const nextBaseVersion = lastDelta.baseVersion + lastDelta.operations.length;
-			const deltas = this.editor.document.history.getDeltas( nextBaseVersion );
+			const deltas = this.editor.model.document.history.getDeltas( nextBaseVersion );
 
 			this._restoreSelection( item.selection.ranges, item.selection.isBackward, deltas );
 			this._undo( item.batch );

+ 8 - 54
packages/ckeditor5-undo/src/undo.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,14 +8,14 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import UndoEngine from './undoengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-
-import undoIcon from '../theme/icons/undo.svg';
-import redoIcon from '../theme/icons/redo.svg';
+import UndoEditing from './undoediting';
+import UndoUI from './undoui';
 
 /**
- * The undo feature. It introduces the Undo and Redo buttons to the editor.
+ * The undo feature.
+ *
+ * It loads the {@link module:undo/undoediting~UndoEditing undo editing feature}
+ * and {@link module:undo/undoui~UndoUI undo UI feature}.
  *
  * Below is the explanation of the undo mechanism working together with {@link module:engine/model/history~History History}:
  *
@@ -106,7 +106,7 @@ export default class Undo extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ UndoEngine ];
+		return [ UndoEditing, UndoUI ];
 	}
 
 	/**
@@ -115,50 +115,4 @@ export default class Undo extends Plugin {
 	static get pluginName() {
 		return 'Undo';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	init() {
-		const editor = this.editor;
-		const t = editor.t;
-
-		this._addButton( 'undo', t( 'Undo' ), 'CTRL+Z', undoIcon );
-		this._addButton( 'redo', t( 'Redo' ), 'CTRL+Y', redoIcon );
-
-		editor.keystrokes.set( 'CTRL+Z', 'undo' );
-		editor.keystrokes.set( 'CTRL+Y', 'redo' );
-		editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' );
-	}
-
-	/**
-	 * Creates a button for the specified command.
-	 *
-	 * @private
-	 * @param {String} name Command name.
-	 * @param {String} label Button label.
-	 * @param {String} keystroke Command keystroke.
-	 * @param {String} Icon Source of the icon.
-	 */
-	_addButton( name, label, keystroke, Icon ) {
-		const editor = this.editor;
-		const command = editor.commands.get( name );
-
-		editor.ui.componentFactory.add( name, locale => {
-			const view = new ButtonView( locale );
-
-			view.set( {
-				label,
-				icon: Icon,
-				keystroke,
-				tooltip: true
-			} );
-
-			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
-
-			this.listenTo( view, 'execute', () => editor.execute( name ) );
-
-			return view;
-		} );
-	}
 }

+ 4 - 4
packages/ckeditor5-undo/src/undocommand.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -34,12 +34,12 @@ export default class UndoCommand extends BaseCommand {
 
 		const item = this._stack.splice( batchIndex, 1 )[ 0 ];
 
-		// All changes has to be done in one `enqueueChanges` callback so other listeners will not
+		// All changes has to be done in one `enqueueChange` callback so other listeners will not
 		// step between consecutive deltas, or won't do changes to the document before selection is properly restored.
-		this.editor.document.enqueueChanges( () => {
+		this.editor.model.enqueueChange( () => {
 			const undoingBatch = this._undo( item.batch );
 
-			const deltas = this.editor.document.history.getDeltas( item.batch.baseVersion );
+			const deltas = this.editor.model.document.history.getDeltas( item.batch.baseVersion );
 			this._restoreSelection( item.selection.ranges, item.selection.isBackward, deltas );
 
 			this.fire( 'revert', item.batch, undoingBatch );

+ 30 - 11
packages/ckeditor5-undo/src/undoengine.js → packages/ckeditor5-undo/src/undoediting.js

@@ -1,10 +1,10 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /**
- * @module undo/undoengine
+ * @module undo/undoediting
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -15,11 +15,11 @@ import RedoCommand from './redocommand';
  * The undo engine feature.
  *
  * Undo brings in possibility to undo and redo changes done in the model by deltas through
- * the {@link module:engine/model/document~Document#batch Batch API}.
+ * the {@link module:engine/model/writer~Writer Writer API}.
  *
  * @extends module:core/plugin~Plugin
  */
-export default class UndoEngine extends Plugin {
+export default class UndoEditing extends Plugin {
 	/**
 	 * @inheritDoc
 	 */
@@ -31,7 +31,7 @@ export default class UndoEngine extends Plugin {
 		 * Created and registered during the {@link #init feature initialization}.
 		 *
 		 * @private
-		 * @member {undo.UndoEngineCommand} #_undoCommand
+		 * @member {module:undo/undocommand~UndoCommand} #_undoCommand
 		 */
 
 		/**
@@ -39,7 +39,7 @@ export default class UndoEngine extends Plugin {
 		 * Created and registered during the {@link #init feature initialization}.
 		 *
 		 * @private
-		 * @member {undo.UndoEngineCommand} #_redoCommand
+		 * @member {module:undo/undocommand~UndoCommand} #_redoCommand
 		 */
 
 		/**
@@ -55,15 +55,30 @@ export default class UndoEngine extends Plugin {
 	 * @inheritDoc
 	 */
 	init() {
+		const editor = this.editor;
+
 		// Create commands.
-		this._undoCommand = new UndoCommand( this.editor );
-		this._redoCommand = new RedoCommand( this.editor );
+		this._undoCommand = new UndoCommand( editor );
+		this._redoCommand = new RedoCommand( editor );
 
 		// Register command to the editor.
-		this.editor.commands.add( 'undo', this._undoCommand );
-		this.editor.commands.add( 'redo', this._redoCommand );
+		editor.commands.add( 'undo', this._undoCommand );
+		editor.commands.add( 'redo', this._redoCommand );
+
+		this.listenTo( editor.model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			// Do not register batch if the operation is not a document operation.
+			// This prevents from creating empty undo steps, where all operations where non-document operations.
+			// Non-document operations creates and alters content in detached tree fragments (for example, document fragments).
+			// Most of time this is preparing data before it is inserted into actual tree (for example during copy & paste).
+			// Such operations should not be reversed.
+			if ( !operation.isDocumentOperation ) {
+				return;
+			}
+
+			const batch = operation.delta.batch;
 
-		this.listenTo( this.editor.document, 'change', ( evt, type, changes, batch ) => {
 			// If changes are not a part of a batch or this is not a new batch, omit those changes.
 			if ( this._batchRegistry.has( batch ) || batch.type == 'transparent' ) {
 				return;
@@ -86,5 +101,9 @@ export default class UndoEngine extends Plugin {
 		this.listenTo( this._undoCommand, 'revert', ( evt, undoneBatch, undoingBatch ) => {
 			this._redoCommand.addBatch( undoingBatch );
 		} );
+
+		editor.keystrokes.set( 'CTRL+Z', 'undo' );
+		editor.keystrokes.set( 'CTRL+Y', 'redo' );
+		editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' );
 	}
 }

+ 63 - 0
packages/ckeditor5-undo/src/undoui.js

@@ -0,0 +1,63 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module undo/undoui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import undoIcon from '../theme/icons/undo.svg';
+import redoIcon from '../theme/icons/redo.svg';
+
+/**
+ * The undo UI feature. It introduces the Undo and Redo buttons to the editor.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class UndoUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		this._addButton( 'undo', t( 'Undo' ), 'CTRL+Z', undoIcon );
+		this._addButton( 'redo', t( 'Redo' ), 'CTRL+Y', redoIcon );
+	}
+
+	/**
+	 * Creates a button for the specified command.
+	 *
+	 * @private
+	 * @param {String} name Command name.
+	 * @param {String} label Button label.
+	 * @param {String} keystroke Command keystroke.
+	 * @param {String} Icon Source of the icon.
+	 */
+	_addButton( name, label, keystroke, Icon ) {
+		const editor = this.editor;
+
+		editor.ui.componentFactory.add( name, locale => {
+			const command = editor.commands.get( name );
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label,
+				icon: Icon,
+				keystroke,
+				tooltip: true
+			} );
+
+			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
+
+			this.listenTo( view, 'execute', () => editor.execute( name ) );
+
+			return view;
+		} );
+	}
+}

+ 5 - 6
packages/ckeditor5-undo/tests/basecommand.js

@@ -1,19 +1,18 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
 import BaseCommand from '../src/basecommand';
 
 describe( 'BaseCommand', () => {
-	let editor, doc, base;
+	let editor, base;
 
 	beforeEach( () => {
 		editor = new ModelTestEditor();
 		base = new BaseCommand( editor );
-
-		doc = editor.document;
 	} );
 
 	afterEach( () => {
@@ -32,7 +31,7 @@ describe( 'BaseCommand', () => {
 		} );
 
 		it( 'should be true if there are batches in command stack', () => {
-			base.addBatch( doc.batch() );
+			base.addBatch( new Batch() );
 
 			expect( base.isEnabled ).to.be.true;
 		} );
@@ -40,7 +39,7 @@ describe( 'BaseCommand', () => {
 
 	describe( 'clearStack', () => {
 		it( 'should remove all batches from the stack', () => {
-			base.addBatch( doc.batch() );
+			base.addBatch( new Batch() );
 			base.clearStack();
 
 			expect( base.isEnabled ).to.be.false;

+ 2 - 2
packages/ckeditor5-undo/tests/manual/undo.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -17,7 +17,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Enter, Typing, Paragraph, Heading, Undo, Bold, Italic ],
-		toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 	} )
 	.then( editor => {
 		window.editor = editor;

+ 48 - 31
packages/ckeditor5-undo/tests/redocommand.js

@@ -1,25 +1,26 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
 import UndoCommand from '../src/undocommand';
 import RedoCommand from '../src/redocommand';
 import { itemAt, getText } from '@ckeditor/ckeditor5-engine/tests/model/_utils/utils';
 
 describe( 'RedoCommand', () => {
-	let editor, doc, root, redo, undo;
+	let editor, model, root, redo, undo;
 
 	beforeEach( () => {
 		editor = new ModelTestEditor();
 		redo = new RedoCommand( editor );
 
-		doc = editor.document;
+		model = editor.model;
 
-		root = doc.getRoot();
+		root = model.document.getRoot();
 	} );
 
 	afterEach( () => {
@@ -49,10 +50,14 @@ describe( 'RedoCommand', () => {
 				 [root]
 				 - {}
 				 */
-				editor.document.selection.setRanges( [ r( 0, 0 ) ] );
-				batch0 = doc.batch();
+				model.change( writer => {
+					writer.setSelection( r( 0, 0 ) );
+				} );
+				batch0 = new Batch();
 				undo.addBatch( batch0 );
-				batch0.insert( p( 0 ), 'foobar' );
+				model.enqueueChange( batch0, writer => {
+					writer.insertText( 'foobar', p( 0 ) );
+				} );
 
 				/*
 				 [root]
@@ -64,10 +69,14 @@ describe( 'RedoCommand', () => {
 				 - r{}
 				 */
 				// Let's make things spicy and this time, make a backward selection.
-				editor.document.selection.setRanges( [ r( 2, 4 ) ], true );
-				batch1 = doc.batch();
+				model.change( writer => {
+					writer.setSelection( r( 2, 4 ), { backward: true } );
+				} );
+				batch1 = new Batch();
 				undo.addBatch( batch1 );
-				batch1.setAttribute( r( 2, 4 ), 'key', 'value' );
+				model.enqueueChange( batch1, writer => {
+					writer.setAttribute( 'key', 'value', r( 2, 4 ) );
+				} );
 
 				/*
 				 [root]
@@ -78,10 +87,14 @@ describe( 'RedoCommand', () => {
 				 - a
 				 - r
 				 */
-				editor.document.selection.setRanges( [ r( 1, 3 ) ] );
-				batch2 = doc.batch();
+				model.change( writer => {
+					writer.setSelection( r( 1, 3 ) );
+				} );
+				batch2 = new Batch();
 				undo.addBatch( batch2 );
-				batch2.move( r( 1, 3 ), p( 6 ) );
+				model.enqueueChange( batch2, writer => {
+					writer.move( r( 1, 3 ), p( 6 ) );
+				} );
 
 				/*
 				 [root]
@@ -112,8 +125,8 @@ describe( 'RedoCommand', () => {
 				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
 
-				expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 			} );
 
 			it( 'should redo series of batches undone by undo command', () => {
@@ -135,8 +148,8 @@ describe( 'RedoCommand', () => {
 				expect( getText( root ) ).to.equal( 'foobar' );
 				expect( Array.from( root.getChildren() ).find( node => node.hasAttribute( 'key' ) ) ).to.be.undefined;
 
-				expect( editor.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 
 				redo.execute();
 				// Should be like after applying `batch1`:
@@ -153,8 +166,8 @@ describe( 'RedoCommand', () => {
 				expect( itemAt( root, 2 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
 
-				expect( editor.document.selection.getRanges().next().value.isEqual( r( 2, 4 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.true;
+				expect( editor.model.document.selection.getRanges().next().value.isEqual( r( 2, 4 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.true;
 
 				redo.execute();
 				// Should be like after applying `batch2`:
@@ -171,8 +184,8 @@ describe( 'RedoCommand', () => {
 				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
 
-				expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 			} );
 
 			it( 'should redo batch selectively undone by undo command', () => {
@@ -193,8 +206,8 @@ describe( 'RedoCommand', () => {
 				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
 
-				expect( editor.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getRanges().next().value.isEqual( r( 6, 6 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 			} );
 
 			it( 'should redo batch selectively undone by undo command #2', () => {
@@ -217,8 +230,8 @@ describe( 'RedoCommand', () => {
 				expect( itemAt( root, 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
 
-				expect( editor.document.selection.getRanges().next().value.isEqual( r( 1, 2 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.true;
+				expect( editor.model.document.selection.getRanges().next().value.isEqual( r( 1, 2 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.true;
 			} );
 
 			it( 'should transform redo batch by changes written in history that happened after undo but before redo #2', () => {
@@ -227,13 +240,17 @@ describe( 'RedoCommand', () => {
 				undo.execute();
 
 				// Remove "ar".
-				doc.batch().remove( r( 4, 6 ) );
+				model.change( writer => {
+					writer.remove( r( 4, 6 ) );
+				} );
 
 				// Undo setting attribute on "ob". Now it is "foob".
 				undo.execute();
 
 				// Append "xx" at the beginning. Now it is "xxfoob".
-				doc.batch().insert( p( 0 ), 'xx' );
+				model.change( writer => {
+					writer.insertText( 'xx', p( 0 ) );
+				} );
 
 				// Redo setting attribute on "ob". Now it is "xxfoOB".
 				redo.execute();
@@ -241,8 +258,8 @@ describe( 'RedoCommand', () => {
 				expect( getText( root ) ).to.equal( 'xxfoob' );
 				expect( itemAt( root, 4 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-				expect( editor.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.true;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.true;
 
 				// Redo moving "oo". Now it is "xxfBoO". Selection is expected to be on just moved "oO".
 				redo.execute();
@@ -250,8 +267,8 @@ describe( 'RedoCommand', () => {
 				expect( getText( root ) ).to.equal( 'xxfboo' );
 				expect( itemAt( root, 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
-				expect( editor.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 			} );
 		} );
 	} );

+ 7 - 126
packages/ckeditor5-undo/tests/undo.js

@@ -1,137 +1,18 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
-/* globals document */
-
-import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import Undo from '../src/undo';
-import UndoEngine from '../src/undoengine';
-import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
-
-testUtils.createSinonSandbox();
+import UndoEditing from '../src/undoediting';
+import UndoUI from '../src/undoui';
 
 describe( 'Undo', () => {
-	let editor, editorElement;
-
-	beforeEach( () => {
-		editorElement = document.createElement( 'div' );
-		document.body.appendChild( editorElement );
-
-		return ClassicTestEditor.create( editorElement, { plugins: [ Undo ] } )
-			.then( newEditor => {
-				editor = newEditor;
-			} );
-	} );
-
-	afterEach( () => {
-		editorElement.remove();
-
-		return editor.destroy();
-	} );
-
-	it( 'should be loaded', () => {
-		expect( editor.plugins.get( Undo ) ).to.be.instanceOf( Undo );
-	} );
-
-	it( 'should load UndoEngine', () => {
-		expect( editor.plugins.get( UndoEngine ) ).to.be.instanceOf( UndoEngine );
-	} );
-
-	testButton( 'undo', 'Undo', 'CTRL+Z' );
-	testButton( 'redo', 'Redo', 'CTRL+Y' );
-
-	it( 'should set CTRL+Z keystroke', () => {
-		const spy = sinon.stub( editor, 'execute' );
-
-		const wasHandled = editor.keystrokes.press( {
-			keyCode: keyCodes.z,
-			ctrlKey: true,
-			preventDefault: sinon.spy(),
-			stopPropagation: sinon.spy()
-		} );
-
-		expect( wasHandled ).to.be.true;
-		expect( spy.calledWithExactly( 'undo' ) ).to.be.true;
-	} );
-
-	it( 'should set CTRL+Y keystroke', () => {
-		const spy = sinon.stub( editor, 'execute' );
-
-		const wasHandled = editor.keystrokes.press( {
-			keyCode: keyCodes.y,
-			ctrlKey: true,
-			preventDefault: sinon.spy(),
-			stopPropagation: sinon.spy()
-		} );
-
-		expect( wasHandled ).to.be.true;
-		expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
+	it( 'should be named', () => {
+		expect( Undo.pluginName ).to.equal( 'Undo' );
 	} );
 
-	it( 'should set CTRL+SHIFT+Z keystroke', () => {
-		const spy = sinon.stub( editor, 'execute' );
-		const keyEventData = {
-			keyCode: keyCodes.z,
-			ctrlKey: true,
-			shiftKey: true,
-			preventDefault: sinon.spy(),
-			stopPropagation: sinon.spy()
-		};
-
-		const wasHandled = editor.keystrokes.press( keyEventData );
-
-		expect( wasHandled ).to.be.true;
-		expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
-		expect( keyEventData.preventDefault.calledOnce ).to.be.true;
+	it( 'should require UndoEditing and UndoUI', () => {
+		expect( Undo.requires ).to.deep.equal( [ UndoEditing, UndoUI ] );
 	} );
-
-	function testButton( featureName, label, featureKeystroke ) {
-		describe( `${ featureName } button`, () => {
-			let button;
-
-			beforeEach( () => {
-				button = editor.ui.componentFactory.create( featureName );
-			} );
-
-			it( 'should register feature component', () => {
-				expect( button ).to.be.instanceOf( ButtonView );
-			} );
-
-			it( 'should create UI component with correct attribute values', () => {
-				expect( button.isOn ).to.be.false;
-				expect( button.label ).to.equal( label );
-				expect( button.icon ).to.match( /<svg / );
-				expect( button.keystroke ).to.equal( featureKeystroke );
-			} );
-
-			it( `should execute ${ featureName } command on model execute event`, () => {
-				const executeSpy = testUtils.sinon.stub( editor, 'execute' );
-
-				button.fire( 'execute' );
-
-				sinon.assert.calledOnce( executeSpy );
-				sinon.assert.calledWithExactly( executeSpy, featureName );
-			} );
-
-			it( `should bind model to ${ featureName } command`, () => {
-				const command = editor.commands.get( featureName );
-
-				expect( button.isOn ).to.be.false;
-
-				const initState = command.isEnabled;
-				expect( button.isEnabled ).to.equal( initState );
-
-				command.isEnabled = !initState;
-				expect( button.isEnabled ).to.equal( !initState );
-			} );
-
-			it( 'should set keystroke in the model', () => {
-				expect( button.keystroke ).to.equal( featureKeystroke );
-			} );
-		} );
-	}
 } );

+ 94 - 42
packages/ckeditor5-undo/tests/undocommand.js

@@ -1,23 +1,24 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
-import Text from '@ckeditor/ckeditor5-engine/src/model/text';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
 import UndoCommand from '../src/undocommand';
 import { itemAt, getText } from '@ckeditor/ckeditor5-engine/tests/model/_utils/utils';
 
 describe( 'UndoCommand', () => {
-	let editor, doc, root, undo;
+	let editor, model, doc, root, undo;
 
 	beforeEach( () => {
 		editor = new ModelTestEditor();
 		undo = new UndoCommand( editor );
 
-		doc = editor.document;
+		model = editor.model;
+		doc = model.document;
 
 		root = doc.getRoot();
 	} );
@@ -38,10 +39,14 @@ describe( 'UndoCommand', () => {
 				 [root]
 				 - {}
 				 */
-				editor.document.selection.setRanges( [ r( 0, 0 ) ] );
-				batch0 = doc.batch();
+				model.change( writer => {
+					writer.setSelection( r( 0, 0 ) );
+				} );
+				batch0 = new Batch();
 				undo.addBatch( batch0 );
-				batch0.insert( p( 0 ), 'foobar' );
+				model.enqueueChange( batch0, writer => {
+					writer.insertText( 'foobar', p( 0 ) );
+				} );
 
 				/*
 				 [root]
@@ -53,10 +58,14 @@ describe( 'UndoCommand', () => {
 				 - r{}
 				 */
 				// Let's make things spicy and this time, make a backward selection.
-				editor.document.selection.setRanges( [ r( 2, 4 ) ], true );
-				batch1 = doc.batch();
+				model.change( writer => {
+					writer.setSelection( r( 2, 4 ), { backward: true } );
+				} );
+				batch1 = new Batch();
 				undo.addBatch( batch1 );
-				batch1.setAttribute( r( 2, 4 ), 'key', 'value' );
+				model.enqueueChange( batch1, writer => {
+					writer.setAttribute( 'key', 'value', r( 2, 4 ) );
+				} );
 
 				/*
 				 [root]
@@ -67,10 +76,14 @@ describe( 'UndoCommand', () => {
 				 - a
 				 - r
 				 */
-				editor.document.selection.setRanges( [ r( 1, 3 ) ] );
-				batch2 = doc.batch();
+				model.change( writer => {
+					writer.setSelection( r( 1, 3 ) );
+				} );
+				batch2 = new Batch();
 				undo.addBatch( batch2 );
-				batch2.move( r( 1, 3 ), p( 6 ) );
+				model.enqueueChange( batch2, writer => {
+					writer.move( r( 1, 3 ), p( 6 ) );
+				} );
 
 				/*
 				 [root]
@@ -81,10 +94,14 @@ describe( 'UndoCommand', () => {
 				 - {o
 				 - o} (key: value)
 				 */
-				editor.document.selection.setRanges( [ r( 1, 4 ) ] );
-				batch3 = doc.batch();
+				model.change( writer => {
+					writer.setSelection( r( 1, 4 ) );
+				} );
+				batch3 = new Batch();
 				undo.addBatch( batch3 );
-				batch3.wrap( r( 1, 4 ), 'p' );
+				model.enqueueChange( batch3, writer => {
+					writer.wrap( r( 1, 4 ), 'p' );
+				} );
 
 				/*
 				 [root]
@@ -96,8 +113,12 @@ describe( 'UndoCommand', () => {
 				 - o
 				 - o (key: value)
 				 */
-				editor.document.selection.setRanges( [ r( 0, 1 ) ] );
-				batch2.move( r( 0, 1 ), p( 3 ) );
+				model.change( writer => {
+					writer.setSelection( r( 0, 1 ) );
+				} );
+				model.enqueueChange( batch2, writer => {
+					writer.move( r( 0, 1 ), p( 3 ) );
+				} );
 
 				/*
 				 [root]
@@ -109,7 +130,9 @@ describe( 'UndoCommand', () => {
 				 - f
 				 - o{} (key: value)
 				 */
-				editor.document.selection.setRanges( [ r( 4, 4 ) ] );
+				model.change( writer => {
+					writer.setSelection( r( 4, 4 ) );
+				} );
 			} );
 
 			it( 'should revert changes done by deltas from the batch that was most recently added to the command stack', () => {
@@ -130,8 +153,8 @@ describe( 'UndoCommand', () => {
 				expect( itemAt( root, 0 ).getAttribute( 'key' ) ).to.equal( 'value' );
 				expect( itemAt( root, 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
 
-				expect( editor.document.selection.getFirstRange().isEqual( r( 0, 3 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 0, 3 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 
 				undo.execute();
 
@@ -152,8 +175,8 @@ describe( 'UndoCommand', () => {
 
 				// Since selection restoring is not 100% accurate, selected range is not perfectly correct
 				// with what is expected in comment above. The correct result would be if range was [ 1 ] - [ 3 ].
-				expect( editor.document.selection.getFirstRange().isEqual( r( 0, 3 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 0, 3 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 
 				undo.execute();
 
@@ -172,8 +195,8 @@ describe( 'UndoCommand', () => {
 				expect( itemAt( root, 2 ).hasAttribute( 'key' ) ).to.be.false;
 				expect( itemAt( root, 3 ).hasAttribute( 'key' ) ).to.be.false;
 
-				expect( editor.document.selection.getFirstRange().isEqual( r( 2, 4 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.true;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 2, 4 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.true;
 
 				undo.execute();
 
@@ -183,7 +206,7 @@ describe( 'UndoCommand', () => {
 				 */
 
 				expect( root.childCount ).to.equal( 0 );
-				expect( editor.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
 			} );
 
 			it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert set attribute)', () => {
@@ -210,8 +233,8 @@ describe( 'UndoCommand', () => {
 
 				// Selection is only partially restored because the range got broken.
 				// The selection would have to best on letter "b" and letter "o", but it is set only on letter "b".
-				expect( editor.document.selection.getFirstRange().isEqual( r( [ 0, 0 ], [ 0, 1 ] ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.true;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( [ 0, 0 ], [ 0, 1 ] ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.true;
 			} );
 
 			it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert insert foobar)', () => {
@@ -228,8 +251,8 @@ describe( 'UndoCommand', () => {
 				expect( root.childCount ).to.equal( 1 );
 				expect( itemAt( root, 0 ).name ).to.equal( 'p' );
 
-				expect( editor.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 
 				undo.execute( batch1 );
 				// Remove attributes.
@@ -240,8 +263,8 @@ describe( 'UndoCommand', () => {
 				expect( itemAt( root, 0 ).name ).to.equal( 'p' );
 
 				// Operations for undoing that batch were working on graveyard so document selection should not change.
-				expect( editor.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 1, 1 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
 
 				expect( doc.graveyard.maxOffset ).to.equal( 6 );
 
@@ -253,8 +276,26 @@ describe( 'UndoCommand', () => {
 				undo.execute( batch3 );
 				expect( root.maxOffset ).to.equal( 0 );
 
-				expect( editor.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
-				expect( editor.document.selection.isBackward ).to.be.false;
+				expect( editor.model.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true;
+				expect( editor.model.document.selection.isBackward ).to.be.false;
+			} );
+
+			it( 'should omit deltas with non-document operations', () => {
+				let element;
+
+				model.change( writer => {
+					element = writer.createElement( 'p' );
+
+					undo.addBatch( writer.batch );
+
+					writer.setAttribute( 'foo', 'bar', element );
+					writer.setAttribute( 'foo', 'bar', root );
+
+					undo.execute();
+				} );
+
+				expect( element.getAttribute( 'foo' ) ).to.equal( 'bar' );
+				expect( root.getAttribute( 'foo' ) ).to.not.equal( 'bar' );
 			} );
 		} );
 
@@ -270,19 +311,30 @@ describe( 'UndoCommand', () => {
 				return text;
 			}
 
-			root.appendChildren( new Text( 'abcdef' ) );
+			model.change( writer => {
+				writer.appendText( 'abcdef', root );
+			} );
+
 			expect( getCaseText( root ) ).to.equal( 'abcdef' );
 
-			editor.document.selection.setRanges( [ r( 1, 4 ) ] );
-			const batch0 = doc.batch();
+			model.change( writer => {
+				writer.setSelection( r( 1, 4 ) );
+			} );
+			const batch0 = new Batch();
 			undo.addBatch( batch0 );
-			batch0.setAttribute( r( 1, 4 ), 'uppercase', true );
+			model.enqueueChange( batch0, writer => {
+				writer.setAttribute( 'uppercase', true, r( 1, 4 ) );
+			} );
 			expect( getCaseText( root ) ).to.equal( 'aBCDef' );
 
-			editor.document.selection.setRanges( [ r( 3, 4 ) ] );
-			const batch1 = doc.batch();
+			model.change( writer => {
+				writer.setSelection( r( 3, 4 ) );
+			} );
+			const batch1 = new Batch();
 			undo.addBatch( batch1 );
-			batch1.move( r( 3, 4 ), p( 1 ) );
+			model.enqueueChange( batch1, writer => {
+				writer.move( r( 3, 4 ), p( 1 ) );
+			} );
 			expect( getCaseText( root ) ).to.equal( 'aDBCef' );
 
 			undo.execute( batch0 );
@@ -290,7 +342,7 @@ describe( 'UndoCommand', () => {
 			// After undo-attr: acdbef <--- "cdb" should be selected, it would look weird if only "cd" or "b" is selected
 			// but the whole unbroken part "cdb" changed attribute.
 			expect( getCaseText( root ) ).to.equal( 'adbcef' );
-			expect( editor.document.selection.getFirstRange().isEqual( r( 1, 4 ) ) ).to.be.true;
+			expect( editor.model.document.selection.getFirstRange().isEqual( r( 1, 4 ) ) ).to.be.true;
 		} );
 	} );
 } );

+ 160 - 115
packages/ckeditor5-undo/tests/undoengine-integration.js → packages/ckeditor5-undo/tests/undoediting-integration.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,53 +9,57 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
-import Element from '@ckeditor/ckeditor5-engine/src/model/element';
-import UndoEngine from '../src/undoengine';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
+import UndoEditing from '../src/undoediting';
 
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import HeadingEngine from '@ckeditor/ckeditor5-heading/src/headingengine';
+import HeadingEditing from '@ckeditor/ckeditor5-heading/src/headingediting';
 import Typing from '@ckeditor/ckeditor5-typing/src/typing';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
-import BoldEngine from '@ckeditor/ckeditor5-basic-styles/src/boldengine';
+import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
 
-import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
 
 import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-describe( 'UndoEngine integration', () => {
-	let editor, doc, root, div;
+describe( 'UndoEditing integration', () => {
+	let editor, model, doc, root, div;
 
 	beforeEach( () => {
 		div = document.createElement( 'div' );
 		document.body.appendChild( div );
 
-		return ClassicEditor.create( div, { plugins: [ Paragraph, HeadingEngine, Typing, Enter, Clipboard, BoldEngine, UndoEngine ] } )
+		return ClassicEditor.create( div, { plugins: [ Paragraph, HeadingEditing, Typing, Enter, Clipboard, BoldEditing, UndoEditing ] } )
 			.then( newEditor => {
 				editor = newEditor;
 
-				doc = editor.document;
+				model = editor.model;
+				doc = model.document;
 
 				// Add "div feature".
-				doc.schema.registerItem( 'div', '$block' );
-				buildModelConverter().for( editor.data.modelToView, editor.editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
-				buildViewConverter().for( editor.data.viewToModel ).fromElement( 'div' ).toElement( 'div' );
+				model.schema.register( 'div', { inheritAllFrom: '$block' } );
+
+				editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'div', view: 'div' } ) );
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'div', view: 'div' } ) );
 
 				root = doc.getRoot();
 			} );
 	} );
 
 	function setSelection( pathA, pathB ) {
-		doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] );
+		model.change( writer => {
+			writer.setSelection( new Range( new Position( root, pathA ), new Position( root, pathB ) ) );
+		} );
 	}
 
 	function input( input ) {
-		setData( doc, input );
+		setData( model, input );
 	}
 
 	function output( output ) {
-		expect( getData( doc ) ).to.equal( output );
+		expect( getData( model ) ).to.equal( output );
 	}
 
 	function undoDisabled() {
@@ -70,8 +74,8 @@ describe( 'UndoEngine integration', () => {
 		it( 'add and undo', () => {
 			input( '<paragraph>fo[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			model.change( writer => {
+				writer.insertText( 'zzz', doc.selection.getFirstPosition() );
 			} );
 			output( '<paragraph>fozzz[]o</paragraph><paragraph>bar</paragraph>' );
 
@@ -84,17 +88,16 @@ describe( 'UndoEngine integration', () => {
 		it( 'multiple adding and undo', () => {
 			input( '<paragraph>fo[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch()
-					.insert( doc.selection.getFirstPosition(), 'zzz' )
-					.insert( new Position( root, [ 1, 0 ] ), 'xxx' );
+			model.change( writer => {
+				writer.insertText( 'zzz', doc.selection.getFirstPosition() );
+				writer.insertText( 'xxx', new Position( root, [ 1, 0 ] ) );
 			} );
 
 			output( '<paragraph>fozzz[]o</paragraph><paragraph>xxxbar</paragraph>' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 1, 0 ], [ 1, 0 ] );
-				doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
+				writer.insertText( 'yyy', doc.selection.getFirstPosition() );
 			} );
 
 			output( '<paragraph>fozzzo</paragraph><paragraph>yyy[]xxxbar</paragraph>' );
@@ -111,14 +114,14 @@ describe( 'UndoEngine integration', () => {
 		it( 'multiple adding mixed with undo', () => {
 			input( '<paragraph>fo[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			model.change( writer => {
+				writer.insertText( 'zzz', doc.selection.getFirstPosition() );
 			} );
 			output( '<paragraph>fozzz[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 1, 0 ], [ 1, 0 ] );
-				doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
+				writer.insertText( 'yyy', doc.selection.getFirstPosition() );
 			} );
 
 			output( '<paragraph>fozzzo</paragraph><paragraph>yyy[]bar</paragraph>' );
@@ -126,9 +129,9 @@ describe( 'UndoEngine integration', () => {
 			editor.execute( 'undo' );
 			output( '<paragraph>fozzzo</paragraph><paragraph>[]bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 0, 0 ], [ 0, 0 ] );
-				doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' );
+				writer.insertText( 'xxx', doc.selection.getFirstPosition() );
 			} );
 			output( '<paragraph>xxx[]fozzzo</paragraph><paragraph>bar</paragraph>' );
 
@@ -144,14 +147,14 @@ describe( 'UndoEngine integration', () => {
 		it( 'multiple remove and undo', () => {
 			input( '<paragraph>[]foo</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
+			model.change( writer => {
+				writer.remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
 			} );
 			output( '<paragraph>[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 1, 1 ], [ 1, 1 ] );
-				doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
+				writer.remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
 			} );
 			output( '<paragraph>o</paragraph><paragraph>b[]</paragraph>' );
 
@@ -169,14 +172,14 @@ describe( 'UndoEngine integration', () => {
 		it( 'add and remove different parts and undo', () => {
 			input( '<paragraph>fo[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			model.change( writer => {
+				writer.insertText( 'zzz', doc.selection.getFirstPosition() );
 			} );
 			output( '<paragraph>fozzz[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 1, 2 ], [ 1, 2 ] );
-				doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ), 1 ) );
+				writer.remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ), 1 ) );
 			} );
 			output( '<paragraph>fozzzo</paragraph><paragraph>b[]r</paragraph>' );
 
@@ -192,13 +195,13 @@ describe( 'UndoEngine integration', () => {
 		it( 'add and remove same part and undo', () => {
 			input( '<paragraph>fo[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			model.change( writer => {
+				writer.insertText( 'zzz', doc.selection.getFirstPosition() );
 			} );
 			output( '<paragraph>fozzz[]o</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ), 3 ) );
+			model.change( writer => {
+				writer.remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ), 3 ) );
 			} );
 			output( '<paragraph>fo[]o</paragraph><paragraph>bar</paragraph>' );
 
@@ -214,8 +217,8 @@ describe( 'UndoEngine integration', () => {
 		it( 'undo remove all content', () => {
 			input( '<paragraph>foo[]</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( Range.createIn( root ) );
+			model.change( writer => {
+				writer.remove( Range.createIn( root ) );
 			} );
 			output( '<paragraph>[]</paragraph>' ); // All hail our king and savior, autoparagraphing!
 
@@ -227,14 +230,17 @@ describe( 'UndoEngine integration', () => {
 
 		it( 'undo insert first content', () => {
 			input( '' );
+			output( '<paragraph>[]</paragraph>' ); // All hail our king and savior, autoparagraphing!
 
-			doc.enqueueChanges( () => {
-				doc.batch().insert( doc.selection.getFirstPosition(), new Element( 'heading1' ) );
+			model.change( writer => {
+				writer.remove( Range.createIn( root ) );
+				writer.insertElement( 'heading1', doc.selection.getFirstPosition() );
 			} );
+
 			output( '<heading1>[]</heading1>' );
 
 			editor.execute( 'undo' );
-			output( '<paragraph>[]</paragraph>' ); // All hail our king and savior, autoparagraphing!
+			output( '<paragraph>[]</paragraph>' );
 
 			undoDisabled();
 		} );
@@ -246,11 +252,10 @@ describe( 'UndoEngine integration', () => {
 			const p = root.getChild( 0 );
 			const pos = new Position( root, [ 0 ] );
 
-			doc.enqueueChanges( () => {
-				doc.batch()
-					.remove( p )
-					.insert( pos, new Element( 'heading1' ) )
-					.insert( pos.getShiftedBy( 1 ), new Element( 'heading2' ) );
+			model.change( writer => {
+				writer.remove( p );
+				writer.insertElement( 'heading1', pos );
+				writer.insertElement( 'heading2', pos.getShiftedBy( 1 ) );
 			} );
 
 			output( '<heading1>[]</heading1><heading2></heading2>' );
@@ -267,13 +272,13 @@ describe( 'UndoEngine integration', () => {
 		it( 'move same content twice then undo', () => {
 			input( '<paragraph>f[o]z</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
+			model.change( writer => {
+				writer.move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
 			} );
 			output( '<paragraph>fz</paragraph><paragraph>[o]bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
+			model.change( writer => {
+				writer.move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
 			} );
 			output( '<paragraph>fz[o]</paragraph><paragraph>bar</paragraph>' );
 
@@ -289,14 +294,14 @@ describe( 'UndoEngine integration', () => {
 		it( 'move content and new parent then undo', () => {
 			input( '<paragraph>f[o]z</paragraph><paragraph>bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
+			model.change( writer => {
+				writer.move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
 			} );
 			output( '<paragraph>fz</paragraph><paragraph>[o]bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 1 ], [ 2 ] );
-				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
+				writer.move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
 			} );
 			output( '[<paragraph>obar</paragraph>]<paragraph>fz</paragraph>' );
 
@@ -314,24 +319,26 @@ describe( 'UndoEngine integration', () => {
 		it( 'attributes then insert inside then undo', () => {
 			input( '<paragraph>fo[ob]ar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().setAttribute( doc.selection.getFirstRange(), 'bold', true );
+			model.change( writer => {
+				writer.setAttribute( 'bold', true, doc.selection.getFirstRange() );
 			} );
 			output( '<paragraph>fo[<$text bold="true">ob</$text>]ar</paragraph>' );
+			expect( doc.selection.getAttribute( 'bold' ) ).to.be.true;
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 0, 3 ], [ 0, 3 ] );
-				doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+				writer.insertText( 'zzz', doc.selection.getFirstPosition() );
 			} );
-			output( '<paragraph>fo<$text bold="true">o</$text>zzz<$text bold="true">[]b</$text>ar</paragraph>' );
-			expect( doc.selection.getAttribute( 'bold' ) ).to.true;
+			output( '<paragraph>fo<$text bold="true">o</$text>zzz[]<$text bold="true">b</$text>ar</paragraph>' );
+			expect( doc.selection.getAttribute( 'bold' ) ).to.be.undefined;
 
 			editor.execute( 'undo' );
 			output( '<paragraph>fo<$text bold="true">o[]b</$text>ar</paragraph>' );
-			expect( doc.selection.getAttribute( 'bold' ) ).to.true;
+			expect( doc.selection.getAttribute( 'bold' ) ).to.be.true;
 
 			editor.execute( 'undo' );
 			output( '<paragraph>fo[ob]ar</paragraph>' );
+			expect( doc.selection.getAttribute( 'bold' ) ).to.be.undefined;
 
 			undoDisabled();
 		} );
@@ -339,11 +346,11 @@ describe( 'UndoEngine integration', () => {
 
 	describe( 'wrapping, unwrapping, merging, splitting', () => {
 		it( 'wrap and undo', () => {
-			doc.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 			input( 'fo[zb]ar' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().wrap( doc.selection.getFirstRange(), 'paragraph' );
+			model.change( writer => {
+				writer.wrap( doc.selection.getFirstRange(), 'paragraph' );
 			} );
 			output( 'fo<paragraph>[zb]</paragraph>ar' );
 
@@ -354,18 +361,18 @@ describe( 'UndoEngine integration', () => {
 		} );
 
 		it( 'wrap, move and undo', () => {
-			doc.schema.allow( { name: '$text', inside: '$root' } );
+			model.schema.extend( '$text', { allowIn: '$root' } );
 			input( 'fo[zb]ar' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().wrap( doc.selection.getFirstRange(), 'paragraph' );
+			model.change( writer => {
+				writer.wrap( doc.selection.getFirstRange(), 'paragraph' );
 			} );
 			// Would be better if selection was inside P.
 			output( 'fo<paragraph>[zb]</paragraph>ar' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				setSelection( [ 2, 0 ], [ 2, 1 ] );
-				doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
+				writer.move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
 			} );
 			output( '[z]fo<paragraph>b</paragraph>ar' );
 
@@ -381,8 +388,8 @@ describe( 'UndoEngine integration', () => {
 		it( 'unwrap and undo', () => {
 			input( '<paragraph>foo[]bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().unwrap( doc.selection.getFirstPosition().parent );
+			model.change( writer => {
+				writer.unwrap( doc.selection.getFirstPosition().parent );
 			} );
 			output( 'foo[]bar' );
 
@@ -395,8 +402,8 @@ describe( 'UndoEngine integration', () => {
 		it( 'merge and undo', () => {
 			input( '<paragraph>foo</paragraph><paragraph>[]bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().merge( new Position( root, [ 1 ] ) );
+			model.change( writer => {
+				writer.merge( new Position( root, [ 1 ] ) );
 				// Because selection is stuck with <paragraph> it ends up in graveyard. We have to manually move it to correct node.
 				setSelection( [ 0, 3 ], [ 0, 3 ] );
 			} );
@@ -411,8 +418,8 @@ describe( 'UndoEngine integration', () => {
 		it( 'split and undo', () => {
 			input( '<paragraph>foo[]bar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				doc.batch().split( doc.selection.getFirstPosition() );
+			model.change( writer => {
+				writer.split( doc.selection.getFirstPosition() );
 				// Because selection is stuck with <paragraph> it ends up in wrong node. We have to manually move it to correct node.
 				setSelection( [ 1, 0 ], [ 1, 0 ] );
 			} );
@@ -770,13 +777,19 @@ describe( 'UndoEngine integration', () => {
 		it( 'rename leaks to other elements on undo #1', () => {
 			input( '<heading1>[]Foo</heading1><paragraph>Bar</paragraph>' );
 
-			doc.batch().rename( root.getChild( 0 ), 'paragraph' );
+			model.change( writer => {
+				writer.rename( root.getChild( 0 ), 'paragraph' );
+			} );
 			output( '<paragraph>[]Foo</paragraph><paragraph>Bar</paragraph>' );
 
-			doc.batch().split( Position.createAt( root.getChild( 0 ), 1 ) );
+			model.change( writer => {
+				writer.split( Position.createAt( root.getChild( 0 ), 1 ) );
+			} );
 			output( '<paragraph>[]F</paragraph><paragraph>oo</paragraph><paragraph>Bar</paragraph>' );
 
-			doc.batch().merge( Position.createAt( root, 2 ) );
+			model.change( writer => {
+				writer.merge( Position.createAt( root, 2 ) );
+			} );
 			output( '<paragraph>[]F</paragraph><paragraph>ooBar</paragraph>' );
 
 			editor.execute( 'undo' );
@@ -793,10 +806,14 @@ describe( 'UndoEngine integration', () => {
 		it( 'rename leaks to other elements on undo #2', () => {
 			input( '<heading1>[]Foo</heading1><paragraph>Bar</paragraph>' );
 
-			doc.batch().rename( root.getChild( 0 ), 'heading2' );
+			model.change( writer => {
+				writer.rename( root.getChild( 0 ), 'heading2' );
+			} );
 			output( '<heading2>[]Foo</heading2><paragraph>Bar</paragraph>' );
 
-			doc.batch().merge( Position.createAt( root, 1 ) );
+			model.change( writer => {
+				writer.merge( Position.createAt( root, 1 ) );
+			} );
 			output( '<heading2>[]FooBar</heading2>' );
 
 			editor.execute( 'undo' );
@@ -810,10 +827,14 @@ describe( 'UndoEngine integration', () => {
 		it( 'merge, rename, undo, undo is correct', () => {
 			input( '<heading1>[]Foo</heading1><paragraph>Bar</paragraph>' );
 
-			doc.batch().merge( Position.createAt( root, 1 ) );
+			model.change( writer => {
+				writer.merge( Position.createAt( root, 1 ) );
+			} );
 			output( '<heading1>[]FooBar</heading1>' );
 
-			doc.batch().rename( root.getChild( 0 ), 'heading2' );
+			model.change( writer => {
+				writer.rename( root.getChild( 0 ), 'heading2' );
+			} );
 			output( '<heading2>[]FooBar</heading2>' );
 
 			editor.execute( 'undo' );
@@ -827,10 +848,14 @@ describe( 'UndoEngine integration', () => {
 		it( 'wrap, split, undo, undo is correct', () => {
 			input( '<paragraph>[]Foo</paragraph><paragraph>Bar</paragraph>' );
 
-			doc.batch().wrap( Range.createIn( root ), 'div' );
+			model.change( writer => {
+				writer.wrap( Range.createIn( root ), 'div' );
+			} );
 			output( '<div><paragraph>[]Foo</paragraph><paragraph>Bar</paragraph></div>' );
 
-			doc.batch().split( new Position( root, [ 0, 0, 1 ] ) );
+			model.change( writer => {
+				writer.split( new Position( root, [ 0, 0, 1 ] ) );
+			} );
 			output( '<div><paragraph>[]F</paragraph><paragraph>oo</paragraph><paragraph>Bar</paragraph></div>' );
 
 			editor.execute( 'undo' );
@@ -877,10 +902,10 @@ describe( 'UndoEngine integration', () => {
 
 			editor.execute( 'enter' );
 
-			doc.enqueueChanges( () => {
+			model.change( writer => {
 				const range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 1, 3 ] ) );
 
-				doc.selection.setRanges( [ range ] );
+				writer.setSelection( range );
 
 				editor.execute( 'delete' );
 			} );
@@ -895,9 +920,9 @@ describe( 'UndoEngine integration', () => {
 		} );
 	} );
 
-	describe( 'pasting', () => {
+	describe( 'clipboard', () => {
 		function pasteHtml( editor, html ) {
-			editor.editing.view.fire( 'paste', {
+			editor.editing.view.document.fire( 'paste', {
 				dataTransfer: createDataTransfer( { 'text/html': html } ),
 				preventDefault() {}
 			} );
@@ -907,23 +932,24 @@ describe( 'UndoEngine integration', () => {
 			return {
 				getData( type ) {
 					return data[ type ];
-				}
+				},
+				setData() {}
 			};
 		}
 
 		// ckeditor5-engine#t/1065
 		it( 'undo paste into non empty element should not throw and be correct', () => {
-			doc.enqueueChanges( () => {
+			model.change( () => {
 				input( '<paragraph>Foo[]</paragraph>' );
 			} );
 
-			doc.enqueueChanges( () => {
+			model.change( () => {
 				pasteHtml( editor, '<p>a</p><p>b</p>' );
 			} );
 
 			output( '<paragraph>Fooa</paragraph><paragraph>b[]</paragraph>' );
 
-			doc.enqueueChanges( () => {
+			model.change( () => {
 				pasteHtml( editor, '<p>c</p><p>d</p>' );
 			} );
 
@@ -935,15 +961,29 @@ describe( 'UndoEngine integration', () => {
 			editor.execute( 'undo' );
 			output( '<paragraph>Foo[]</paragraph>' );
 		} );
+
+		// ckeditor5#781
+		it( 'cutting should not create empty undo step', () => {
+			input( '<paragraph>Fo[oba]r</paragraph>' );
+
+			editor.editing.view.document.fire( 'cut', {
+				dataTransfer: createDataTransfer(),
+				preventDefault() {},
+				method: 'cut'
+			} );
+
+			editor.execute( 'undo' );
+
+			output( '<paragraph>Fo[oba]r</paragraph>' );
+			undoDisabled();
+		} );
 	} );
 
 	describe( 'other edge cases', () => {
 		it( 'deleteContent between two nodes', () => {
 			input( '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
 
-			doc.enqueueChanges( () => {
-				editor.data.deleteContent( doc.selection, doc.batch() );
-			} );
+			editor.model.deleteContent( doc.selection );
 			output( '<paragraph>fo[]ar</paragraph>' );
 
 			editor.execute( 'undo' );
@@ -957,9 +997,12 @@ describe( 'UndoEngine integration', () => {
 			const gy = doc.graveyard;
 			const p = doc.getRoot().getChild( 0 );
 
-			doc.enqueueChanges( () => {
-				doc.batch().remove( p );
-				doc.batch().setAttribute( p, 'bold', true );
+			model.change( writer => {
+				writer.remove( p );
+			} );
+
+			model.change( writer => {
+				writer.setAttribute( 'bold', true, p );
 			} );
 
 			editor.execute( 'undo' );
@@ -976,20 +1019,22 @@ describe( 'UndoEngine integration', () => {
 			input( '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
 
 			// Remove children from graveyard because they are inserted there after `input` call.
-			doc.graveyard.removeChildren( 0, doc.graveyard.childCount );
+			model.change( writer => {
+				writer.remove( Range.createIn( doc.graveyard ) );
+			} );
 
-			const batchWithMerge = doc.batch();
+			const batchWithMerge = new Batch();
 
-			doc.enqueueChanges( () => {
-				batchWithMerge.merge( new Position( root, [ 1 ] ) );
+			model.enqueueChange( batchWithMerge, writer => {
+				writer.merge( new Position( root, [ 1 ] ) );
+			} );
 
-				const split = batchWithMerge.deltas[ 0 ].getReversed();
-				const batch = doc.batch();
-				batch.addDelta( split );
+			const split = batchWithMerge.deltas[ 0 ].getReversed();
+			const batch = new Batch();
+			batch.addDelta( split );
 
-				doc.applyOperation( split.operations[ 0 ] );
-				doc.applyOperation( split.operations[ 1 ] );
-			} );
+			model.applyOperation( split.operations[ 0 ] );
+			model.applyOperation( split.operations[ 1 ] );
 
 			output( '<paragraph>[]Foo</paragraph><paragraph>Bar</paragraph>' );
 

+ 154 - 0
packages/ckeditor5-undo/tests/undoediting.js

@@ -0,0 +1,154 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
+import UndoEditing from '../src/undoediting';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+
+describe( 'UndoEditing', () => {
+	let editor, undo, model, root;
+
+	beforeEach( () => {
+		editor = new ModelTestEditor();
+
+		model = editor.model;
+		root = model.document.getRoot();
+
+		undo = new UndoEditing( editor );
+		undo.init();
+	} );
+
+	afterEach( () => {
+		undo.destroy();
+	} );
+
+	it( 'should register undo command and redo command', () => {
+		expect( editor.commands.get( 'undo' ) ).to.equal( undo._undoCommand );
+		expect( editor.commands.get( 'redo' ) ).to.equal( undo._redoCommand );
+	} );
+
+	it( 'should add a batch to undo command and clear redo stack, if it\'s type is "default"', () => {
+		sinon.spy( undo._undoCommand, 'addBatch' );
+		sinon.spy( undo._redoCommand, 'clearStack' );
+
+		expect( undo._undoCommand.addBatch.called ).to.be.false;
+		expect( undo._redoCommand.clearStack.called ).to.be.false;
+
+		model.change( writer => {
+			writer.insertText( 'foobar', root );
+		} );
+
+		expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
+		expect( undo._redoCommand.clearStack.calledOnce ).to.be.true;
+	} );
+
+	it( 'should add each batch only once', () => {
+		sinon.spy( undo._undoCommand, 'addBatch' );
+
+		model.change( writer => {
+			writer.insertText( 'foobar', root );
+			writer.insertText( 'foobar', root );
+		} );
+
+		expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
+	} );
+
+	it( 'should add a batch to undo command, if it\'s type is undo and it comes from redo command', () => {
+		sinon.spy( undo._undoCommand, 'addBatch' );
+		sinon.spy( undo._redoCommand, 'clearStack' );
+
+		const batch = new Batch();
+
+		undo._redoCommand._createdBatches.add( batch );
+
+		model.enqueueChange( batch, writer => {
+			writer.insertText( 'foobar', root );
+		} );
+
+		expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
+		expect( undo._redoCommand.clearStack.called ).to.be.false;
+	} );
+
+	it( 'should add a batch to redo command on undo revert event', () => {
+		sinon.spy( undo._redoCommand, 'addBatch' );
+		sinon.spy( undo._redoCommand, 'clearStack' );
+
+		undo._undoCommand.fire( 'revert', null, new Batch() );
+
+		expect( undo._redoCommand.addBatch.calledOnce ).to.be.true;
+		expect( undo._redoCommand.clearStack.called ).to.be.false;
+	} );
+
+	it( 'should not add a batch that has only non-document operations', () => {
+		sinon.spy( undo._undoCommand, 'addBatch' );
+
+		model.change( writer => {
+			const docFrag = writer.createDocumentFragment();
+			const element = writer.createElement( 'paragraph' );
+			writer.insert( element, docFrag, 0 );
+			writer.insertText( 'foo', null, element, 0 );
+		} );
+
+		expect( undo._undoCommand.addBatch.called ).to.be.false;
+	} );
+
+	it( 'should add a batch that has both document and non-document operations', () => {
+		sinon.spy( undo._undoCommand, 'addBatch' );
+
+		model.change( writer => {
+			const element = writer.createElement( 'paragraph' );
+			writer.insertText( 'foo', null, element, 0 );
+			writer.insert( element, root, 0 );
+		} );
+
+		expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
+	} );
+
+	it( 'should set CTRL+Z keystroke', () => {
+		const spy = sinon.stub( editor, 'execute' );
+
+		const wasHandled = editor.keystrokes.press( {
+			keyCode: keyCodes.z,
+			ctrlKey: true,
+			preventDefault: sinon.spy(),
+			stopPropagation: sinon.spy()
+		} );
+
+		expect( wasHandled ).to.be.true;
+		expect( spy.calledWithExactly( 'undo' ) ).to.be.true;
+	} );
+
+	it( 'should set CTRL+Y keystroke', () => {
+		const spy = sinon.stub( editor, 'execute' );
+
+		const wasHandled = editor.keystrokes.press( {
+			keyCode: keyCodes.y,
+			ctrlKey: true,
+			preventDefault: sinon.spy(),
+			stopPropagation: sinon.spy()
+		} );
+
+		expect( wasHandled ).to.be.true;
+		expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
+	} );
+
+	it( 'should set CTRL+SHIFT+Z keystroke', () => {
+		const spy = sinon.stub( editor, 'execute' );
+		const keyEventData = {
+			keyCode: keyCodes.z,
+			ctrlKey: true,
+			shiftKey: true,
+			preventDefault: sinon.spy(),
+			stopPropagation: sinon.spy()
+		};
+
+		const wasHandled = editor.keystrokes.press( keyEventData );
+
+		expect( wasHandled ).to.be.true;
+		expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
+		expect( keyEventData.preventDefault.calledOnce ).to.be.true;
+	} );
+} );

+ 0 - 77
packages/ckeditor5-undo/tests/undoengine.js

@@ -1,77 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import Position from '@ckeditor/ckeditor5-engine/src/model/position';
-import UndoEngine from '../src/undoengine';
-
-describe( 'UndoEngine', () => {
-	let editor, undo, batch, doc, root;
-
-	beforeEach( () => {
-		editor = new ModelTestEditor();
-
-		doc = editor.document;
-		batch = doc.batch();
-		root = doc.getRoot();
-
-		undo = new UndoEngine( editor );
-		undo.init();
-	} );
-
-	afterEach( () => {
-		undo.destroy();
-	} );
-
-	describe( 'UndoEngine', () => {
-		it( 'should register undo command and redo command', () => {
-			expect( editor.commands.get( 'undo' ) ).to.equal( undo._undoCommand );
-			expect( editor.commands.get( 'redo' ) ).to.equal( undo._redoCommand );
-		} );
-
-		it( 'should add a batch to undo command and clear redo stack, if it\'s type is "default"', () => {
-			sinon.spy( undo._undoCommand, 'addBatch' );
-			sinon.spy( undo._redoCommand, 'clearStack' );
-
-			expect( undo._undoCommand.addBatch.called ).to.be.false;
-			expect( undo._redoCommand.clearStack.called ).to.be.false;
-
-			batch.insert( new Position( root, [ 0 ] ), 'foobar' );
-
-			expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
-			expect( undo._redoCommand.clearStack.calledOnce ).to.be.true;
-		} );
-
-		it( 'should add each batch only once', () => {
-			sinon.spy( undo._undoCommand, 'addBatch' );
-
-			batch.insert( new Position( root, [ 0 ] ), 'foobar' ).insert( new Position( root, [ 0 ] ), 'foobar' );
-
-			expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
-		} );
-
-		it( 'should add a batch to undo command, if it\'s type is undo and it comes from redo command', () => {
-			sinon.spy( undo._undoCommand, 'addBatch' );
-			sinon.spy( undo._redoCommand, 'clearStack' );
-
-			undo._redoCommand._createdBatches.add( batch );
-
-			batch.insert( new Position( root, [ 0 ] ), 'foobar' );
-
-			expect( undo._undoCommand.addBatch.calledOnce ).to.be.true;
-			expect( undo._redoCommand.clearStack.called ).to.be.false;
-		} );
-
-		it( 'should add a batch to redo command on undo revert event', () => {
-			sinon.spy( undo._redoCommand, 'addBatch' );
-			sinon.spy( undo._redoCommand, 'clearStack' );
-
-			undo._undoCommand.fire( 'revert', null, batch );
-
-			expect( undo._redoCommand.addBatch.calledOnce ).to.be.true;
-			expect( undo._redoCommand.clearStack.called ).to.be.false;
-		} );
-	} );
-} );

+ 83 - 0
packages/ckeditor5-undo/tests/undoui.js

@@ -0,0 +1,83 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import UndoEditing from '../src/undoediting';
+import UndoUI from '../src/undoui';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'UndoUI', () => {
+	let editor, editorElement;
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		return ClassicTestEditor.create( editorElement, { plugins: [ UndoEditing, UndoUI ] } )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	testButton( 'undo', 'Undo', 'CTRL+Z' );
+	testButton( 'redo', 'Redo', 'CTRL+Y' );
+
+	function testButton( featureName, label, featureKeystroke ) {
+		describe( `${ featureName } button`, () => {
+			let button;
+
+			beforeEach( () => {
+				button = editor.ui.componentFactory.create( featureName );
+			} );
+
+			it( 'should register feature component', () => {
+				expect( button ).to.be.instanceOf( ButtonView );
+			} );
+
+			it( 'should create UI component with correct attribute values', () => {
+				expect( button.isOn ).to.be.false;
+				expect( button.label ).to.equal( label );
+				expect( button.icon ).to.match( /<svg / );
+				expect( button.keystroke ).to.equal( featureKeystroke );
+			} );
+
+			it( `should execute ${ featureName } command on model execute event`, () => {
+				const executeSpy = testUtils.sinon.stub( editor, 'execute' );
+
+				button.fire( 'execute' );
+
+				sinon.assert.calledOnce( executeSpy );
+				sinon.assert.calledWithExactly( executeSpy, featureName );
+			} );
+
+			it( `should bind model to ${ featureName } command`, () => {
+				const command = editor.commands.get( featureName );
+
+				expect( button.isOn ).to.be.false;
+
+				const initState = command.isEnabled;
+				expect( button.isEnabled ).to.equal( initState );
+
+				command.isEnabled = !initState;
+				expect( button.isEnabled ).to.equal( !initState );
+			} );
+
+			it( 'should set keystroke in the model', () => {
+				expect( button.keystroke ).to.equal( featureKeystroke );
+			} );
+		} );
+	}
+} );

+ 1 - 1
packages/ckeditor5-undo/theme/icons/redo.svg

@@ -1 +1 @@
-<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 16l1-3.982c-3.417 0-8.616.869-10 3.982 0-5.983 6.601-7.96 10-7.96 0-.85-1-3.32-1-4.04l8 6-8 6z" fill="#454545" fill-rule="evenodd"/></svg>
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M14.958 9.367l-2.189 1.837a.75.75 0 0 0 .965 1.149l3.788-3.18a.747.747 0 0 0 .21-.284.75.75 0 0 0-.17-.945L13.77 4.762a.75.75 0 1 0-.964 1.15l2.331 1.955H6.22A.75.75 0 0 0 6 7.9a4 4 0 1 0 1.477 7.718l-.344-1.489A2.5 2.5 0 1 1 6.039 9.4l-.008-.032h8.927z" fill="#000" fill-rule="nonzero"/></svg>

+ 1 - 1
packages/ckeditor5-undo/theme/icons/undo.svg

@@ -1 +1 @@
-<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 10l8 6-1-3.982c3.417 0 8.616.869 10 3.982 0-5.983-6.601-7.96-10-7.96 0-.85 1-3.32 1-4.04l-8 6z" fill="#454545" fill-rule="evenodd"/></svg>
+<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M5.042 9.367l2.189 1.837a.75.75 0 0 1-.965 1.149l-3.788-3.18a.747.747 0 0 1-.21-.284.75.75 0 0 1 .17-.945L6.23 4.762a.75.75 0 1 1 .964 1.15L4.863 7.866h8.917A.75.75 0 0 1 14 7.9a4 4 0 1 1-1.477 7.718l.344-1.489a2.5 2.5 0 1 0 1.094-4.73l.008-.032H5.042z" fill="#000" fill-rule="nonzero"/></svg>