Browse Source

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

Release: v1.0.0-beta.1.
Piotrek Koszuliński 7 years ago
parent
commit
0636979ac4
40 changed files with 1161 additions and 906 deletions
  1. 1 1
      packages/ckeditor5-typing/.eslintrc.js
  2. 15 11
      packages/ckeditor5-typing/.travis.yml
  3. 13 0
      packages/ckeditor5-typing/CHANGELOG.md
  4. 1 1
      packages/ckeditor5-typing/LICENSE.md
  5. 3 1
      packages/ckeditor5-typing/README.md
  6. 18 15
      packages/ckeditor5-typing/package.json
  7. 25 36
      packages/ckeditor5-typing/src/changebuffer.js
  8. 6 5
      packages/ckeditor5-typing/src/delete.js
  9. 24 23
      packages/ckeditor5-typing/src/deletecommand.js
  10. 17 4
      packages/ckeditor5-typing/src/deleteobserver.js
  11. 26 11
      packages/ckeditor5-typing/src/input.js
  12. 9 8
      packages/ckeditor5-typing/src/inputcommand.js
  13. 1 1
      packages/ckeditor5-typing/src/typing.js
  14. 19 19
      packages/ckeditor5-typing/tests/bogusbr-integration.js
  15. 41 18
      packages/ckeditor5-typing/tests/changebuffer.js
  16. 9 9
      packages/ckeditor5-typing/tests/delete.js
  17. 76 23
      packages/ckeditor5-typing/tests/deletecommand-integration.js
  18. 69 56
      packages/ckeditor5-typing/tests/deletecommand.js
  19. 65 7
      packages/ckeditor5-typing/tests/deleteobserver.js
  20. 151 540
      packages/ckeditor5-typing/tests/input.js
  21. 15 14
      packages/ckeditor5-typing/tests/inputcommand-integration.js
  22. 41 40
      packages/ckeditor5-typing/tests/inputcommand.js
  23. 2 2
      packages/ckeditor5-typing/tests/manual/20/1.js
  24. 2 2
      packages/ckeditor5-typing/tests/manual/21/1.js
  25. 2 2
      packages/ckeditor5-typing/tests/manual/40/1.js
  26. 3 3
      packages/ckeditor5-typing/tests/manual/52/1.js
  27. 1 1
      packages/ckeditor5-typing/tests/manual/59/1.js
  28. 3 3
      packages/ckeditor5-typing/tests/manual/82/1.js
  29. 2 2
      packages/ckeditor5-typing/tests/manual/86/1.js
  30. 3 3
      packages/ckeditor5-typing/tests/manual/delete.js
  31. 3 3
      packages/ckeditor5-typing/tests/manual/input.js
  32. 7 7
      packages/ckeditor5-typing/tests/manual/nbsp.js
  33. 8 8
      packages/ckeditor5-typing/tests/manual/rtl.js
  34. 1 1
      packages/ckeditor5-typing/tests/manual/selection.js
  35. 3 3
      packages/ckeditor5-typing/tests/manual/spellchecking.js
  36. 1 1
      packages/ckeditor5-typing/tests/manual/unicode.js
  37. 16 14
      packages/ckeditor5-typing/tests/spellchecking.js
  38. 451 0
      packages/ckeditor5-typing/tests/tickets/100.js
  39. 7 7
      packages/ckeditor5-typing/tests/tickets/59.js
  40. 1 1
      packages/ckeditor5-typing/tests/typing.js

+ 1 - 1
packages/ckeditor5-typing/.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 

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

@@ -1,24 +1,28 @@
 sudo: required
 sudo: required
 dist: trusty
 dist: trusty
 addons:
 addons:
+  firefox: "latest"
   apt:
   apt:
     sources:
     sources:
-      - google-chrome
+    - google-chrome
     packages:
     packages:
-      - google-chrome-stable
+    - google-chrome-stable
 language: node_js
 language: node_js
 node_js:
 node_js:
-  - "6"
+- '6'
 cache:
 cache:
-  - node_modules
+- node_modules
 before_install:
 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:
 install:
-  - npm install @ckeditor/ckeditor5-dev-tests
-  - ckeditor5-dev-tests-install-dependencies
+- npm install @ckeditor/ckeditor5-dev-tests
+- ckeditor5-dev-tests-install-dependencies
 script:
 script:
-  - ckeditor5-dev-tests-travis
+- ckeditor5-dev-tests-travis
 after_success:
 after_success:
-  - codeclimate-test-reporter < coverage/lcov.info
-  - ckeditor5-dev-tests-save-revision
+- ckeditor5-dev-tests-save-revision
+env:
+  global:
+  - secure: sgO6VC3Bn9qK+da6p4nF5jj+fYUaDQRUchajKd2GsxRl0zjxljuoxQCYi0lhI2mlcfvNSDt+aF/cYFczECt6pf8vTudXeCEfM1ecAf4UQ5NVgIp71bBZdxrKyVwbe37EYL+IO2KFMnZqH1OR6WS0TW9VOt6Ia8PBsZSvOXQ4C8WCc+uEkFqUdfvkqy3t2CNYGILa8MVQs85WJTe9NFRQqVve67S8ayL4UvYpXHdeYDCgbt7CbhQU7jwIYNmw1B0JsQOWXxE7vJiKsn60fLP7+V3S23jfPI+r2pWW6lCTBrUM7/u06JUoze1SrAAlVjY4c3/tYEfiiUjNvfNphC501Orpaub7u2U74ZTOlUgAMy9MLCKBV28gQfyR5erFBFkSifbHRD6BxXLpmjtC03RIxyBNHPxLkzSz1Bz+oe6rxuS4J6f+M17ZF6e5E3AcI76hpNZgTSHOqnHAZ6IgEiOMLL/070JWBoq40uHaOhDWYvTEHB/hYpiLMnVP00gABb8tvrtQst3woFRFJqQzmu/EMAkMGXgpvPaTpm2FYJCfOPKoLvutVoqZR18IoSKEZ88mLsSco5J1t2NKeczukKMP1po0+lXgkRj/xtqEXAz9HWPDmi2rBXk+qvBeMNT6d4YPV5fk1YoIC3xPEtmPOOEhz+dWoc7MpOFeULVNwrqlOXQ=
+  - secure: I48fZzp10VFbXD4pPvjdxSM7VKo9mkrQ2Bu/JP5zSwdRlVEW+vJI/yKC6t5//5gCp/XC0vibaxdmbg73fvJUYvcAGc/yBKChGfR1sjt10kTQYcUwbuVuE4vlStbD3td4vhEZMOL4k0b11tNsuUe43WLJsqdQ3/Znr6sD/+VAlJBUXKUQRGhpAwgw0rpL5U/3wCPbhjmA4SSMUlWjPIPFeWIUcCBEWruk2WjZ3OgYrjcnSw1tQ3LNZgYydaS1XT5MQbU9YeIEmTKWOXJVCTZ8hGYdeubZ10v9KrkFj/lny3m5XiU+Ae+rhV0LJpFuYl1XD897xXrquo4zTSk+Txu95NxfMFdLtt9nfZ+2y+oEUx8qUGxO+dXeg9vtITaa6QL/kWxofmprDurSZqGW+BicSRK7FxShhQZtJ4VKYe2976edIeNOqFtpP9iLOuVKt9LIsuqmbDSDMlC7RnVg33FbEiyOsrXzw/3gYQEyVymiUWUWX8vET6k7O8iz25wufHO4tAIZDG44R4HdCSYXlMHXGHqDQjfYXFxIA88hH1mCk3/EVDjn6ZEqf2nw8r5sUU3CPHXHVv/Ap1UylUvkdLb3GF6MTIdICGE/d5cmbx3T63o0xpDYO1SmeZZ/nIicmdgmK2Ny3hMjTCv33J1Ra3bAv0t2/vJ7/dZU+kgMXfgaXYo=

+ 13 - 0
packages/ckeditor5-typing/CHANGELOG.md

@@ -1,6 +1,19 @@
 Changelog
 Changelog
 =========
 =========
 
 
+## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-typing/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
+
+### Bug fixes
+
+* `DeleteObserver` will stop the `keydown` event when the `delete` event is stopped. Closes: https://github.com/ckeditor/ckeditor5/issues/753. ([479d043](https://github.com/ckeditor/ckeditor5-typing/commit/479d043))
+* Editor should not crash in scenarios when mutations' common ancestor could not be mapped to the model. Closes [ckeditor/ckeditor5#718](https://github.com/ckeditor/ckeditor5/issues/718). ([db0fe8f](https://github.com/ckeditor/ckeditor5-typing/commit/db0fe8f))
+* Properly discover delete-word keyboard modifier on mac and non-mac computers. Closes [#92](https://github.com/ckeditor/ckeditor5-typing/issues/92). ([81f5b76](https://github.com/ckeditor/ckeditor5-typing/commit/81f5b76))
+
+### Other changes
+
+* Aligned feature class naming to the new scheme. ([9c2cb9d](https://github.com/ckeditor/ckeditor5-typing/commit/9c2cb9d))
+
+
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-typing/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-typing/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 
 
 Internal changes only (updated dependencies, documentation, etc.).
 Internal changes only (updated dependencies, documentation, etc.).

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

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 ==========================
 
 
 **CKEditor 5 Typing Feature** – https://github.com/ckeditor/ckeditor5-typing <br>
 **CKEditor 5 Typing Feature** – https://github.com/ckeditor/ckeditor5-typing <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:
 Licensed under the terms of any of the following licenses at your choice:
 
 

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

@@ -4,7 +4,9 @@ CKEditor 5 typing 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)
 [![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-typing.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-typing)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-typing.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-typing)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-typing.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-typing)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-typing.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-typing)
-[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-typing/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-typing/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-typing/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-typing?branch=master)
+<br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-typing/status.svg)](https://david-dm.org/ckeditor/ckeditor5-typing)
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-typing/status.svg)](https://david-dm.org/ckeditor/ckeditor5-typing)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-typing/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-typing?type=dev)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-typing/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-typing?type=dev)
 
 

+ 18 - 15
packages/ckeditor5-typing/package.json

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

+ 25 - 36
packages/ckeditor5-typing/src/changebuffer.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -7,7 +7,7 @@
  * @module typing/changebuffer
  * @module typing/changebuffer
  */
  */
 
 
-import count from '@ckeditor/ckeditor5-utils/src/count';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
 
 
 /**
 /**
  * Change buffer allows to group atomic changes (like characters that have been typed) into
  * Change buffer allows to group atomic changes (like characters that have been typed) into
@@ -20,7 +20,7 @@ import count from '@ckeditor/ckeditor5-utils/src/count';
  *
  *
  * To use the change buffer you need to let it know about the number of changes that were added to the batch:
  * To use the change buffer you need to let it know about the number of changes that were added to the batch:
  *
  *
- *		const buffer = new ChangeBuffer( document, LIMIT );
+ *		const buffer = new ChangeBuffer( model, LIMIT );
  *
  *
  *		// Later on in your feature:
  *		// Later on in your feature:
  *		buffer.batch.insert( pos, insertedCharacters );
  *		buffer.batch.insert( pos, insertedCharacters );
@@ -31,17 +31,17 @@ export default class ChangeBuffer {
 	/**
 	/**
 	 * Creates a new instance of the change buffer.
 	 * Creates a new instance of the change buffer.
 	 *
 	 *
-	 * @param {module:engine/model/document~Document} document
+	 * @param {module:engine/model/model~Model} model
 	 * @param {Number} [limit=20] The maximum number of atomic changes which can be contained in one batch.
 	 * @param {Number} [limit=20] The maximum number of atomic changes which can be contained in one batch.
 	 */
 	 */
-	constructor( doc, limit = 20 ) {
+	constructor( model, limit = 20 ) {
 		/**
 		/**
-		 * The document instance.
+		 * The model instance.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:engine/model/document~Document} #document
+		 * @member {module:engine/model/model~Model} #model
 		 */
 		 */
-		this.document = doc;
+		this.model = model;
 
 
 		/**
 		/**
 		 * The number of atomic changes in the buffer. Once it exceeds the {@link #limit},
 		 * The number of atomic changes in the buffer. Once it exceeds the {@link #limit},
@@ -68,19 +68,26 @@ export default class ChangeBuffer {
 		 */
 		 */
 		this.isLocked = false;
 		this.isLocked = false;
 
 
-		this._changeCallback = ( evt, type, changes, batch ) => {
-			this._onBatch( batch );
+		// The function to be called in order to notify the buffer about batches which appeared in the document.
+		// The callback will check whether it is a new batch and in that case the buffer will be flushed.
+		//
+		// The reason why the buffer needs to be flushed whenever a new batch appears is that the changes added afterwards
+		// should be added to a new batch. For instance, when the  user types, then inserts an image, and then types again,
+		// the characters typed after inserting the image should be added to a different batch than the characters typed before.
+		this._changeCallback = ( evt, batch ) => {
+			if ( batch.type != 'transparent' && batch !== this._batch ) {
+				this._reset( true );
+			}
 		};
 		};
 
 
 		this._selectionChangeCallback = () => {
 		this._selectionChangeCallback = () => {
 			this._reset();
 			this._reset();
 		};
 		};
 
 
-		doc.on( 'change', this._changeCallback );
+		this.model.document.on( 'change', this._changeCallback );
 
 
-		doc.selection.on( 'change:range', this._selectionChangeCallback );
-
-		doc.selection.on( 'change:attribute', this._selectionChangeCallback );
+		this.model.document.selection.on( 'change:range', this._selectionChangeCallback );
+		this.model.document.selection.on( 'change:attribute', this._selectionChangeCallback );
 
 
 		/**
 		/**
 		 * The current batch instance.
 		 * The current batch instance.
@@ -112,7 +119,7 @@ export default class ChangeBuffer {
 	 */
 	 */
 	get batch() {
 	get batch() {
 		if ( !this._batch ) {
 		if ( !this._batch ) {
-			this._batch = this.document.batch();
+			this._batch = new Batch();
 		}
 		}
 
 
 		return this._batch;
 		return this._batch;
@@ -150,27 +157,9 @@ export default class ChangeBuffer {
 	 * Destroys the buffer.
 	 * Destroys the buffer.
 	 */
 	 */
 	destroy() {
 	destroy() {
-		this.document.off( 'change', this._changeCallback );
-		this.document.selection.off( 'change:range', this._selectionChangeCallback );
-		this.document.selection.off( 'change:attribute', this._selectionChangeCallback );
-	}
-
-	/**
-	 * The method to be called in order to notify the buffer about batches which appeared in the document.
-	 * The method will check whether it is a new batch and in that case the buffer will be flushed.
-	 *
-	 * The reason why the buffer needs to be flushed whenever a new batch appears is that the changes added afterwards
-	 * should be added to a new batch. For instance, when the  user types, then inserts an image, and then types again,
-	 * the characters typed after inserting the image should be added to a different batch than the characters typed before.
-	 *
-	 * @private
-	 * @param {module:engine/model/batch~Batch} batch The batch which appears in the document.
-	 */
-	_onBatch( batch ) {
-		// One operation means a newly created batch.
-		if ( batch.type != 'transparent' && batch !== this._batch && count( batch.getOperations() ) <= 1 ) {
-			this._reset( true );
-		}
+		this.model.document.off( 'change', this._changeCallback );
+		this.model.document.selection.off( 'change:range', this._selectionChangeCallback );
+		this.model.document.selection.off( 'change:attribute', this._selectionChangeCallback );
 	}
 	}
 
 
 	/**
 	/**

+ 6 - 5
packages/ckeditor5-typing/src/delete.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -26,17 +26,18 @@ export default class Delete extends Plugin {
 
 
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
-		const editingView = editor.editing.view;
+		const view = editor.editing.view;
+		const viewDocument = view.document;
 
 
-		editingView.addObserver( DeleteObserver );
+		view.addObserver( DeleteObserver );
 
 
 		editor.commands.add( 'forwardDelete', new DeleteCommand( editor, 'forward' ) );
 		editor.commands.add( 'forwardDelete', new DeleteCommand( editor, 'forward' ) );
 		editor.commands.add( 'delete', new DeleteCommand( editor, 'backward' ) );
 		editor.commands.add( 'delete', new DeleteCommand( editor, 'backward' ) );
 
 
-		this.listenTo( editingView, 'delete', ( evt, data ) => {
+		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
 			editor.execute( data.direction == 'forward' ? 'forwardDelete' : 'delete', { unit: data.unit, sequence: data.sequence } );
 			editor.execute( data.direction == 'forward' ? 'forwardDelete' : 'delete', { unit: data.unit, sequence: data.sequence } );
 			data.preventDefault();
 			data.preventDefault();
-			editingView.scrollToTheSelection();
+			view.scrollToTheSelection();
 		} );
 		} );
 	}
 	}
 }
 }

+ 24 - 23
packages/ckeditor5-typing/src/deletecommand.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -10,7 +10,6 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import Selection from '@ckeditor/ckeditor5-engine/src/model/selection';
 import Selection from '@ckeditor/ckeditor5-engine/src/model/selection';
 import Element from '@ckeditor/ckeditor5-engine/src/model/element';
 import Element from '@ckeditor/ckeditor5-engine/src/model/element';
-import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import ChangeBuffer from './changebuffer';
 import ChangeBuffer from './changebuffer';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import count from '@ckeditor/ckeditor5-utils/src/count';
@@ -48,7 +47,7 @@ export default class DeleteCommand extends Command {
 		 * @private
 		 * @private
 		 * @member {typing.ChangeBuffer} #buffer
 		 * @member {typing.ChangeBuffer} #buffer
 		 */
 		 */
-		this._buffer = new ChangeBuffer( editor.document, editor.config.get( 'typing.undoStep' ) );
+		this._buffer = new ChangeBuffer( editor.model, editor.config.get( 'typing.undoStep' ) );
 	}
 	}
 
 
 	/**
 	/**
@@ -57,18 +56,18 @@ export default class DeleteCommand extends Command {
 	 *
 	 *
 	 * @fires execute
 	 * @fires execute
 	 * @param {Object} [options] The command options.
 	 * @param {Object} [options] The command options.
-	 * @param {'character'} [options.unit='character'] See {@link module:engine/controller/modifyselection~modifySelection}'s options.
+	 * @param {'character'} [options.unit='character'] See {@link module:engine/model/utils/modifyselection~modifySelection}'s options.
 	 * @param {Number} [options.sequence=1] A number describing which subsequent delete event it is without the key being released.
 	 * @param {Number} [options.sequence=1] A number describing which subsequent delete event it is without the key being released.
 	 * See the {@link module:engine/view/document~Document#event:delete} event data.
 	 * See the {@link module:engine/view/document~Document#event:delete} event data.
 	 */
 	 */
 	execute( options = {} ) {
 	execute( options = {} ) {
-		const doc = this.editor.document;
-		const dataController = this.editor.data;
+		const model = this.editor.model;
+		const doc = model.document;
 
 
-		doc.enqueueChanges( () => {
+		model.enqueueChange( this._buffer.batch, writer => {
 			this._buffer.lock();
 			this._buffer.lock();
 
 
-			const selection = Selection.createFromSelection( doc.selection );
+			const selection = new Selection( doc.selection );
 
 
 			// Do not replace the whole selected content if selection was collapsed.
 			// Do not replace the whole selected content if selection was collapsed.
 			// This prevents such situation:
 			// This prevents such situation:
@@ -79,12 +78,12 @@ export default class DeleteCommand extends Command {
 
 
 			// Try to extend the selection in the specified direction.
 			// Try to extend the selection in the specified direction.
 			if ( selection.isCollapsed ) {
 			if ( selection.isCollapsed ) {
-				dataController.modifySelection( selection, { direction: this.direction, unit: options.unit } );
+				model.modifySelection( selection, { direction: this.direction, unit: options.unit } );
 			}
 			}
 
 
 			// Check if deleting in an empty editor. See #61.
 			// Check if deleting in an empty editor. See #61.
 			if ( this._shouldEntireContentBeReplacedWithParagraph( options.sequence || 1 ) ) {
 			if ( this._shouldEntireContentBeReplacedWithParagraph( options.sequence || 1 ) ) {
-				this._replaceEntireContentWithParagraph();
+				this._replaceEntireContentWithParagraph( writer );
 
 
 				return;
 				return;
 			}
 			}
@@ -102,10 +101,10 @@ export default class DeleteCommand extends Command {
 				);
 				);
 			} );
 			} );
 
 
-			dataController.deleteContent( selection, this._buffer.batch, { doNotResetEntireContent } );
+			model.deleteContent( selection, { doNotResetEntireContent } );
 			this._buffer.input( changeCount );
 			this._buffer.input( changeCount );
 
 
-			doc.selection.setRanges( selection.getRanges(), selection.isBackward );
+			writer.setSelection( selection );
 
 
 			this._buffer.unlock();
 			this._buffer.unlock();
 		} );
 		} );
@@ -135,9 +134,10 @@ export default class DeleteCommand extends Command {
 			return false;
 			return false;
 		}
 		}
 
 
-		const document = this.editor.document;
-		const selection = document.selection;
-		const limitElement = document.schema.getLimitElement( selection );
+		const model = this.editor.model;
+		const doc = model.document;
+		const selection = doc.selection;
+		const limitElement = model.schema.getLimitElement( selection );
 
 
 		// If a collapsed selection contains the whole content it means that the content is empty
 		// If a collapsed selection contains the whole content it means that the content is empty
 		// (from the user perspective).
 		// (from the user perspective).
@@ -147,7 +147,7 @@ export default class DeleteCommand extends Command {
 			return false;
 			return false;
 		}
 		}
 
 
-		if ( !document.schema.check( { name: 'paragraph', inside: limitElement.name } ) ) {
+		if ( !model.schema.checkChild( limitElement, 'paragraph' ) ) {
 			return false;
 			return false;
 		}
 		}
 
 
@@ -168,15 +168,16 @@ export default class DeleteCommand extends Command {
 	 *
 	 *
 	 * @private
 	 * @private
 	 */
 	 */
-	_replaceEntireContentWithParagraph() {
-		const document = this.editor.document;
-		const selection = document.selection;
-		const limitElement = document.schema.getLimitElement( selection );
+	_replaceEntireContentWithParagraph( writer ) {
+		const model = this.editor.model;
+		const doc = model.document;
+		const selection = doc.selection;
+		const limitElement = model.schema.getLimitElement( selection );
 		const paragraph = new Element( 'paragraph' );
 		const paragraph = new Element( 'paragraph' );
 
 
-		this._buffer.batch.remove( Range.createIn( limitElement ) );
-		this._buffer.batch.insert( Position.createAt( limitElement ), paragraph );
+		writer.remove( Range.createIn( limitElement ) );
+		writer.insert( paragraph, limitElement );
 
 
-		selection.setCollapsedAt( paragraph );
+		writer.setSelection( paragraph, 0 );
 	}
 	}
 }
 }

+ 17 - 4
packages/ckeditor5-typing/src/deleteobserver.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -10,6 +10,7 @@
 import Observer from '@ckeditor/ckeditor5-engine/src/view/observer/observer';
 import Observer from '@ckeditor/ckeditor5-engine/src/view/observer/observer';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import env from '@ckeditor/ckeditor5-utils/src/env';
 
 
 /**
 /**
  * Delete observer introduces the {@link module:engine/view/document~Document#event:delete} event.
  * Delete observer introduces the {@link module:engine/view/document~Document#event:delete} event.
@@ -17,9 +18,10 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  * @extends module:engine/view/observer/observer~Observer
  * @extends module:engine/view/observer/observer~Observer
  */
  */
 export default class DeleteObserver extends Observer {
 export default class DeleteObserver extends Observer {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
+		const document = view.document;
 		let sequence = 0;
 		let sequence = 0;
 
 
 		document.on( 'keyup', ( evt, data ) => {
 		document.on( 'keyup', ( evt, data ) => {
@@ -41,10 +43,21 @@ export default class DeleteObserver extends Observer {
 				return;
 				return;
 			}
 			}
 
 
-			deleteData.unit = data.altKey ? 'word' : deleteData.unit;
+			const hasWordModifier = env.isMac ? data.altKey : data.ctrlKey;
+			deleteData.unit = hasWordModifier ? 'word' : deleteData.unit;
 			deleteData.sequence = ++sequence;
 			deleteData.sequence = ++sequence;
 
 
+			// Save the event object to check later if it was stopped or not.
+			let event;
+			document.once( 'delete', evt => ( event = evt ), { priority: 'highest' } );
+
 			document.fire( 'delete', new DomEventData( document, data.domEvent, deleteData ) );
 			document.fire( 'delete', new DomEventData( document, data.domEvent, deleteData ) );
+
+			// Stop `keydown` event if `delete` event was stopped.
+			// https://github.com/ckeditor/ckeditor5/issues/753
+			if ( event && event.stop.called ) {
+				evt.stop();
+			}
 		} );
 		} );
 	}
 	}
 
 

+ 26 - 11
packages/ckeditor5-typing/src/input.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -42,11 +42,11 @@ export default class Input extends Plugin {
 
 
 		editor.commands.add( 'input', inputCommand );
 		editor.commands.add( 'input', inputCommand );
 
 
-		this.listenTo( editingView, 'keydown', ( evt, data ) => {
+		this.listenTo( editingView.document, 'keydown', ( evt, data ) => {
 			this._handleKeydown( data, inputCommand );
 			this._handleKeydown( data, inputCommand );
 		}, { priority: 'lowest' } );
 		}, { priority: 'lowest' } );
 
 
-		this.listenTo( editingView, 'mutations', ( evt, mutations, viewSelection ) => {
+		this.listenTo( editingView.document, 'mutations', ( evt, mutations, viewSelection ) => {
 			this._handleMutations( mutations, viewSelection );
 			this._handleMutations( mutations, viewSelection );
 		} );
 		} );
 	}
 	}
@@ -68,7 +68,8 @@ export default class Input extends Plugin {
 	 * @param {module:typing/inputcommand~InputCommand} inputCommand
 	 * @param {module:typing/inputcommand~InputCommand} inputCommand
 	 */
 	 */
 	_handleKeydown( evtData, inputCommand ) {
 	_handleKeydown( evtData, inputCommand ) {
-		const doc = this.editor.document;
+		const model = this.editor.model;
+		const doc = model.document;
 		const buffer = inputCommand.buffer;
 		const buffer = inputCommand.buffer;
 
 
 		// By relying on the state of the input command we allow disabling the entire input easily
 		// By relying on the state of the input command we allow disabling the entire input easily
@@ -86,8 +87,8 @@ export default class Input extends Plugin {
 
 
 		buffer.lock();
 		buffer.lock();
 
 
-		doc.enqueueChanges( () => {
-			this.editor.data.deleteContent( doc.selection, buffer.batch );
+		model.enqueueChange( buffer.batch, () => {
+			this.editor.model.deleteContent( doc.selection );
 		} );
 		} );
 
 
 		buffer.unlock();
 		buffer.unlock();
@@ -186,18 +187,26 @@ class MutationHandler {
 		// Get common ancestor in DOM.
 		// Get common ancestor in DOM.
 		const domMutationCommonAncestor = domConverter.mapViewToDom( mutationsCommonAncestor );
 		const domMutationCommonAncestor = domConverter.mapViewToDom( mutationsCommonAncestor );
 
 
-		if ( !domMutationCommonAncestor ) {
-			return;
-		}
-
 		// Create fresh DomConverter so it will not use existing mapping and convert current DOM to model.
 		// Create fresh DomConverter so it will not use existing mapping and convert current DOM to model.
 		// This wouldn't be needed if DomConverter would allow to create fresh view without checking any mappings.
 		// This wouldn't be needed if DomConverter would allow to create fresh view without checking any mappings.
 		const freshDomConverter = new DomConverter();
 		const freshDomConverter = new DomConverter();
-		const modelFromCurrentDom = this.editor.data.toModel( freshDomConverter.domToView( domMutationCommonAncestor ) ).getChild( 0 );
+		const modelFromCurrentDom = this.editor.data.toModel(
+			freshDomConverter.domToView( domMutationCommonAncestor )
+		).getChild( 0 );
 
 
 		// Current model.
 		// Current model.
 		const currentModel = this.editor.editing.mapper.toModelElement( mutationsCommonAncestor );
 		const currentModel = this.editor.editing.mapper.toModelElement( mutationsCommonAncestor );
 
 
+		// If common ancestor is not mapped, do not do anything. It probably is a parent of another view element.
+		// That means that we would need to diff model elements (see `if` below). Better return early instead of
+		// trying to get a reasonable model ancestor. It will fell into the `if` below anyway.
+		// This situation happens for example for lists. If `<ul>` is a common ancestor, `currentModel` is `undefined`
+		// because `<ul>` is not mapped (`<li>`s are).
+		// See https://github.com/ckeditor/ckeditor5/issues/718.
+		if ( !currentModel ) {
+			return;
+		}
+
 		// Get children from both ancestors.
 		// Get children from both ancestors.
 		const modelFromDomChildren = Array.from( modelFromCurrentDom.getChildren() );
 		const modelFromDomChildren = Array.from( modelFromCurrentDom.getChildren() );
 		const currentModelChildren = Array.from( currentModel.getChildren() );
 		const currentModelChildren = Array.from( currentModel.getChildren() );
@@ -243,6 +252,9 @@ class MutationHandler {
 		} );
 		} );
 	}
 	}
 
 
+	/**
+	 * @private
+	 */
 	_handleTextMutation( mutation, viewSelection ) {
 	_handleTextMutation( mutation, viewSelection ) {
 		if ( mutation.type != 'text' ) {
 		if ( mutation.type != 'text' ) {
 			return;
 			return;
@@ -285,6 +297,9 @@ class MutationHandler {
 		} );
 		} );
 	}
 	}
 
 
+	/**
+	 * @private
+	 */
 	_handleTextNodeInsertion( mutation ) {
 	_handleTextNodeInsertion( mutation ) {
 		if ( mutation.type != 'children' ) {
 		if ( mutation.type != 'children' ) {
 			return;
 			return;

+ 9 - 8
packages/ckeditor5-typing/src/inputcommand.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -33,7 +33,7 @@ export default class InputCommand extends Command {
 		 * @private
 		 * @private
 		 * @member {module:typing/changebuffer~ChangeBuffer} #_buffer
 		 * @member {module:typing/changebuffer~ChangeBuffer} #_buffer
 		 */
 		 */
-		this._buffer = new ChangeBuffer( editor.document, undoStepSize );
+		this._buffer = new ChangeBuffer( editor.model, undoStepSize );
 	}
 	}
 
 
 	/**
 	/**
@@ -69,30 +69,31 @@ export default class InputCommand extends Command {
 	 * the inserted text.
 	 * the inserted text.
 	 */
 	 */
 	execute( options = {} ) {
 	execute( options = {} ) {
-		const doc = this.editor.document;
+		const model = this.editor.model;
+		const doc = model.document;
 		const text = options.text || '';
 		const text = options.text || '';
 		const textInsertions = text.length;
 		const textInsertions = text.length;
 		const range = options.range || doc.selection.getFirstRange();
 		const range = options.range || doc.selection.getFirstRange();
 		const resultRange = options.resultRange;
 		const resultRange = options.resultRange;
 
 
-		doc.enqueueChanges( () => {
+		model.enqueueChange( this._buffer.batch, writer => {
 			const isCollapsedRange = range.isCollapsed;
 			const isCollapsedRange = range.isCollapsed;
 
 
 			this._buffer.lock();
 			this._buffer.lock();
 
 
 			if ( !isCollapsedRange ) {
 			if ( !isCollapsedRange ) {
-				this._buffer.batch.remove( range );
+				writer.remove( range );
 			}
 			}
 
 
 			if ( text ) {
 			if ( text ) {
-				this._buffer.batch.weakInsert( range.start, text );
+				writer.insertText( text, doc.selection.getAttributes(), range.start );
 			}
 			}
 
 
 			if ( resultRange ) {
 			if ( resultRange ) {
-				this.editor.data.model.selection.setRanges( [ resultRange ] );
+				writer.setSelection( resultRange );
 			} else if ( isCollapsedRange ) {
 			} else if ( isCollapsedRange ) {
 				// If range was collapsed just shift the selection by the number of inserted characters.
 				// If range was collapsed just shift the selection by the number of inserted characters.
-				this.editor.data.model.selection.setCollapsedAt( range.start.getShiftedBy( textInsertions ) );
+				writer.setSelection( range.start.getShiftedBy( textInsertions ) );
 			}
 			}
 
 
 			this._buffer.unlock();
 			this._buffer.unlock();

+ 1 - 1
packages/ckeditor5-typing/src/typing.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 

+ 19 - 19
packages/ckeditor5-typing/tests/bogusbr-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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -44,12 +44,12 @@ describe( 'Typing – bogus BR integration', () => {
 		// a little different in every browser (as native mutations may be different in different browsers).
 		// a little different in every browser (as native mutations may be different in different browsers).
 
 
 		it( 'space is inserted on the end of the line (paragraph)', done => {
 		it( 'space is inserted on the end of the line (paragraph)', done => {
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 				editor.editing.view.getDomRoot().focus();
 				editor.editing.view.getDomRoot().focus();
-				setData( editor.document, '<paragraph>Foo[]</paragraph>' );
+				setData( editor.model, '<paragraph>Foo[]</paragraph>' );
 			} );
 			} );
 
 
-			editor.document.once( 'changesDone', () => {
+			editor.model.document.once( 'change', () => {
 				expect( editor.getData() ).to.equal( '<p>Foo&nbsp;</p>' );
 				expect( editor.getData() ).to.equal( '<p>Foo&nbsp;</p>' );
 				done();
 				done();
 			}, { priority: 'low' } );
 			}, { priority: 'low' } );
@@ -58,12 +58,12 @@ describe( 'Typing – bogus BR integration', () => {
 		} );
 		} );
 
 
 		it( 'space is inserted on the end of the line (empty paragraph)', done => {
 		it( 'space is inserted on the end of the line (empty paragraph)', done => {
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 				editor.editing.view.getDomRoot().focus();
 				editor.editing.view.getDomRoot().focus();
-				setData( editor.document, '<paragraph>[]</paragraph>' );
+				setData( editor.model, '<paragraph>[]</paragraph>' );
 			} );
 			} );
 
 
-			editor.document.once( 'changesDone', () => {
+			editor.model.document.once( 'change', () => {
 				expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
 				expect( editor.getData() ).to.equal( '<p>&nbsp;</p>' );
 				done();
 				done();
 			}, { priority: 'low' } );
 			}, { priority: 'low' } );
@@ -72,12 +72,12 @@ describe( 'Typing – bogus BR integration', () => {
 		} );
 		} );
 
 
 		it( 'space is inserted on the end of the line (bold)', done => {
 		it( 'space is inserted on the end of the line (bold)', done => {
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 				editor.editing.view.getDomRoot().focus();
 				editor.editing.view.getDomRoot().focus();
-				setData( editor.document, '<paragraph><$text bold="true">Foo[]</$text></paragraph>' );
+				setData( editor.model, '<paragraph><$text bold="true">Foo[]</$text></paragraph>' );
 			} );
 			} );
 
 
-			editor.document.once( 'changesDone', () => {
+			editor.model.document.once( 'change', () => {
 				expect( editor.getData() ).to.equal( '<p><strong>Foo&nbsp;</strong></p>' );
 				expect( editor.getData() ).to.equal( '<p><strong>Foo&nbsp;</strong></p>' );
 				done();
 				done();
 			}, { priority: 'low' } );
 			}, { priority: 'low' } );
@@ -90,12 +90,12 @@ describe( 'Typing – bogus BR integration', () => {
 		// This tests use fixed list of mutation mocks to simulate specific browser behaviours.
 		// This tests use fixed list of mutation mocks to simulate specific browser behaviours.
 
 
 		it( 'space is inserted on the end of the paragraph', done => {
 		it( 'space is inserted on the end of the paragraph', done => {
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 				editor.editing.view.getDomRoot().focus();
 				editor.editing.view.getDomRoot().focus();
-				setData( editor.document, '<paragraph>Foo[]</paragraph>' );
+				setData( editor.model, '<paragraph>Foo[]</paragraph>' );
 			} );
 			} );
 
 
-			editor.document.once( 'changesDone', () => {
+			editor.model.document.once( 'change', () => {
 				expect( editor.getData() ).to.equal( '<p>Foo&nbsp;</p>' );
 				expect( editor.getData() ).to.equal( '<p>Foo&nbsp;</p>' );
 				done();
 				done();
 			}, { priority: 'low' } );
 			}, { priority: 'low' } );
@@ -116,12 +116,12 @@ describe( 'Typing – bogus BR integration', () => {
 		} );
 		} );
 
 
 		it( 'space is inserted on the end of the line paragraph (with bogus br)', done => {
 		it( 'space is inserted on the end of the line paragraph (with bogus br)', done => {
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 				editor.editing.view.getDomRoot().focus();
 				editor.editing.view.getDomRoot().focus();
-				setData( editor.document, '<paragraph>Foo[]</paragraph>' );
+				setData( editor.model, '<paragraph>Foo[]</paragraph>' );
 			} );
 			} );
 
 
-			editor.document.once( 'changesDone', () => {
+			editor.model.document.once( 'change', () => {
 				expect( editor.getData() ).to.equal( '<p>Foo&nbsp;</p>' );
 				expect( editor.getData() ).to.equal( '<p>Foo&nbsp;</p>' );
 				done();
 				done();
 			}, { priority: 'low' } );
 			}, { priority: 'low' } );
@@ -145,12 +145,12 @@ describe( 'Typing – bogus BR integration', () => {
 		} );
 		} );
 
 
 		it( 'word is properly corrected on the end of the block element (with bogus br)', done => {
 		it( 'word is properly corrected on the end of the block element (with bogus br)', done => {
-			editor.document.enqueueChanges( () => {
+			editor.model.change( () => {
 				editor.editing.view.getDomRoot().focus();
 				editor.editing.view.getDomRoot().focus();
-				setData( editor.document, '<paragraph>Foo hous[]</paragraph>' );
+				setData( editor.model, '<paragraph>Foo hous[]</paragraph>' );
 			} );
 			} );
 
 
-			editor.document.once( 'changesDone', () => {
+			editor.model.document.once( 'change', () => {
 				expect( editor.getData() ).to.equal( '<p>Foo house</p>' );
 				expect( editor.getData() ).to.equal( '<p>Foo house</p>' );
 				done();
 				done();
 			}, { priority: 'low' } );
 			}, { priority: 'low' } );

+ 41 - 18
packages/ckeditor5-typing/tests/changebuffer.js

@@ -1,33 +1,33 @@
 /**
 /**
- * @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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
 import ChangeBuffer from '../src/changebuffer';
 import ChangeBuffer from '../src/changebuffer';
-import Document from '@ckeditor/ckeditor5-engine/src/model/document';
+import Model from '@ckeditor/ckeditor5-engine/src/model/model';
 import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
 import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
-import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 
 
 describe( 'ChangeBuffer', () => {
 describe( 'ChangeBuffer', () => {
 	const CHANGE_LIMIT = 3;
 	const CHANGE_LIMIT = 3;
-	let doc, buffer, root;
+	let model, doc, buffer, root;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		doc = new Document();
+		model = new Model();
+		doc = model.document;
 		root = doc.createRoot();
 		root = doc.createRoot();
-		buffer = new ChangeBuffer( doc, CHANGE_LIMIT );
+		buffer = new ChangeBuffer( model, CHANGE_LIMIT );
 	} );
 	} );
 
 
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'sets all properties', () => {
 		it( 'sets all properties', () => {
-			expect( buffer ).to.have.property( 'document', doc );
+			expect( buffer ).to.have.property( 'model', model );
 			expect( buffer ).to.have.property( 'limit', CHANGE_LIMIT );
 			expect( buffer ).to.have.property( 'limit', CHANGE_LIMIT );
 			expect( buffer ).to.have.property( 'size', 0 );
 			expect( buffer ).to.have.property( 'size', 0 );
 			expect( buffer ).to.have.property( 'isLocked', false );
 			expect( buffer ).to.have.property( 'isLocked', false );
 		} );
 		} );
 
 
 		it( 'sets limit property according to default value', () => {
 		it( 'sets limit property according to default value', () => {
-			buffer = new ChangeBuffer( doc );
+			buffer = new ChangeBuffer( model );
 
 
 			expect( buffer ).to.have.property( 'limit', 20 );
 			expect( buffer ).to.have.property( 'limit', 20 );
 		} );
 		} );
@@ -91,7 +91,9 @@ describe( 'ChangeBuffer', () => {
 			// Ensure that size is reset too.
 			// Ensure that size is reset too.
 			buffer.input( 1 );
 			buffer.input( 1 );
 
 
-			doc.batch().insert( Position.createAt( root, 0 ), 'a' );
+			model.change( writer => {
+				writer.insertText( 'a', root );
+			} );
 
 
 			expect( buffer.batch ).to.not.equal( batch1 );
 			expect( buffer.batch ).to.not.equal( batch1 );
 			expect( buffer.size ).to.equal( 0 );
 			expect( buffer.size ).to.equal( 0 );
@@ -100,31 +102,48 @@ describe( 'ChangeBuffer', () => {
 		it( 'is not reset when changes are added to the buffer\'s batch', () => {
 		it( 'is not reset when changes are added to the buffer\'s batch', () => {
 			const batch1 = buffer.batch;
 			const batch1 = buffer.batch;
 
 
-			buffer.batch.insert( Position.createAt( root, 0 ), 'a' );
+			model.enqueueChange( buffer.batch, writer => {
+				writer.insert( 'a', root );
+			} );
 			expect( buffer.batch ).to.equal( batch1 );
 			expect( buffer.batch ).to.equal( batch1 );
 
 
-			buffer.batch.insert( Position.createAt( root, 0 ), 'b' );
+			model.enqueueChange( buffer.batch, writer => {
+				writer.insert( 'b', root );
+			} );
 			expect( buffer.batch ).to.equal( batch1 );
 			expect( buffer.batch ).to.equal( batch1 );
 		} );
 		} );
 
 
 		it( 'is not reset when changes are added to batch which existed previously', () => {
 		it( 'is not reset when changes are added to batch which existed previously', () => {
-			const externalBatch = doc.batch();
+			const externalBatch = new Batch();
 
 
-			externalBatch.insert( Position.createAt( root, 0 ), 'a' );
+			model.change( writer => {
+				writer.insertText( 'a', root );
+			} );
+
+			model.enqueueChange( externalBatch, writer => {
+				writer.insertText( 'a', root );
+			} );
 
 
 			const bufferBatch = buffer.batch;
 			const bufferBatch = buffer.batch;
 
 
-			buffer.batch.insert( Position.createAt( root, 0 ), 'b' );
+			model.enqueueChange( bufferBatch, writer => {
+				writer.insertText( 'b', root );
+			} );
+
 			expect( buffer.batch ).to.equal( bufferBatch );
 			expect( buffer.batch ).to.equal( bufferBatch );
 
 
-			doc.batch().insert( Position.createAt( root, 0 ), 'c' );
+			model.change( writer => {
+				writer.insertText( 'c', root );
+			} );
 			expect( buffer.batch ).to.not.equal( bufferBatch );
 			expect( buffer.batch ).to.not.equal( bufferBatch );
 		} );
 		} );
 
 
 		it( 'is not reset when changes are applied in transparent batch', () => {
 		it( 'is not reset when changes are applied in transparent batch', () => {
 			const bufferBatch = buffer.batch;
 			const bufferBatch = buffer.batch;
 
 
-			doc.batch( 'transparent' ).insert( Position.createAt( root, 0 ), 'a' );
+			model.enqueueChange( 'transparent', writer => {
+				writer.insert( 'a', root );
+			} );
 
 
 			expect( buffer.batch ).to.equal( bufferBatch );
 			expect( buffer.batch ).to.equal( bufferBatch );
 		} );
 		} );
@@ -175,7 +194,9 @@ describe( 'ChangeBuffer', () => {
 
 
 			buffer.lock();
 			buffer.lock();
 
 
-			doc.batch().insert( Position.createAt( root, 0 ), 'a' );
+			model.change( writer => {
+				writer.insertText( 'a', root );
+			} );
 
 
 			buffer.unlock();
 			buffer.unlock();
 
 
@@ -236,7 +257,9 @@ describe( 'ChangeBuffer', () => {
 
 
 			buffer.destroy();
 			buffer.destroy();
 
 
-			doc.batch().insert( Position.createAt( root, 0 ), 'a' );
+			model.change( writer => {
+				writer.insertText( 'a', root );
+			} );
 
 
 			expect( buffer.batch ).to.equal( batch1 );
 			expect( buffer.batch ).to.equal( batch1 );
 		} );
 		} );

+ 9 - 9
packages/ckeditor5-typing/tests/delete.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -8,14 +8,14 @@ import Delete from '../src/delete';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 
 
 describe( 'Delete feature', () => {
 describe( 'Delete feature', () => {
-	let editor, editingView;
+	let editor, viewDocument;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		return VirtualTestEditor
 		return VirtualTestEditor
 			.create( { plugins: [ Delete ] } )
 			.create( { plugins: [ Delete ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
-				editingView = editor.editing.view;
+				viewDocument = editor.editing.view.document;
 			} );
 			} );
 	} );
 	} );
 
 
@@ -28,12 +28,12 @@ describe( 'Delete feature', () => {
 		expect( editor.commands.get( 'forwardDelete' ) ).to.have.property( 'direction', 'forward' );
 		expect( editor.commands.get( 'forwardDelete' ) ).to.have.property( 'direction', 'forward' );
 	} );
 	} );
 
 
-	it( 'listens to the editing view delete event', () => {
+	it( 'listens to the editing view document delete event', () => {
 		const spy = editor.execute = sinon.spy();
 		const spy = editor.execute = sinon.spy();
-		const view = editor.editing.view;
+		const viewDocument = editor.editing.view.document;
 		const domEvt = getDomEvent();
 		const domEvt = getDomEvent();
 
 
-		view.fire( 'delete', new DomEventData( editingView, domEvt, {
+		viewDocument.fire( 'delete', new DomEventData( viewDocument, domEvt, {
 			direction: 'forward',
 			direction: 'forward',
 			unit: 'character',
 			unit: 'character',
 			sequence: 1
 			sequence: 1
@@ -44,7 +44,7 @@ describe( 'Delete feature', () => {
 
 
 		expect( domEvt.preventDefault.calledOnce ).to.be.true;
 		expect( domEvt.preventDefault.calledOnce ).to.be.true;
 
 
-		view.fire( 'delete', new DomEventData( editingView, getDomEvent(), {
+		viewDocument.fire( 'delete', new DomEventData( viewDocument, getDomEvent(), {
 			direction: 'backward',
 			direction: 'backward',
 			unit: 'character',
 			unit: 'character',
 			sequence: 5
 			sequence: 5
@@ -55,10 +55,10 @@ describe( 'Delete feature', () => {
 	} );
 	} );
 
 
 	it( 'scrolls the editing document to the selection after executing the command', () => {
 	it( 'scrolls the editing document to the selection after executing the command', () => {
-		const scrollSpy = sinon.stub( editingView, 'scrollToTheSelection' );
+		const scrollSpy = sinon.stub( editor.editing.view, 'scrollToTheSelection' );
 		const executeSpy = editor.execute = sinon.spy();
 		const executeSpy = editor.execute = sinon.spy();
 
 
-		editingView.fire( 'delete', new DomEventData( editingView, getDomEvent(), {
+		viewDocument.fire( 'delete', new DomEventData( viewDocument, getDomEvent(), {
 			direction: 'backward',
 			direction: 'backward',
 			unit: 'character'
 			unit: 'character'
 		} ) );
 		} ) );

+ 76 - 23
packages/ckeditor5-typing/tests/deletecommand-integration.js

@@ -1,39 +1,50 @@
 /**
 /**
- * @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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+/* global document */
+
+import Typing from '../src/typing';
 import DeleteCommand from '../src/deletecommand';
 import DeleteCommand from '../src/deletecommand';
-import UndoEngine from '@ckeditor/ckeditor5-undo/src/undoengine';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
+import Heading from '@ckeditor/ckeditor5-heading/src/heading';
+import List from '@ckeditor/ckeditor5-list/src/list';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Image from '@ckeditor/ckeditor5-image/src/image';
+import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 describe( 'DeleteCommand integration', () => {
 describe( 'DeleteCommand integration', () => {
-	let editor, doc;
+	let editor, model;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		return ModelTestEditor
 		return ModelTestEditor
 			.create( {
 			.create( {
-				plugins: [ UndoEngine ],
+				plugins: [ UndoEditing ],
 				typing: {
 				typing: {
 					undoStep: 3
 					undoStep: 3
 				}
 				}
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
-				doc = editor.document;
+				model = editor.model;
 
 
 				const command = new DeleteCommand( editor, 'backward' );
 				const command = new DeleteCommand( editor, 'backward' );
 				editor.commands.add( 'delete', command );
 				editor.commands.add( 'delete', command );
 
 
 				// Mock paragraph feature.
 				// Mock paragraph feature.
-				doc.schema.registerItem( 'paragraph', '$block' );
-				doc.schema.allow( { name: 'paragraph', inside: '$block' } );
-
-				doc.schema.registerItem( 'img', '$inline' );
-				doc.schema.allow( { name: '$text', inside: 'img' } );
-
-				doc.schema.objects.add( 'img' );
+				model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				model.schema.extend( 'paragraph', { allowIn: '$block' } );
+
+				model.schema.register( 'img', {
+					allowWhere: '$text',
+					isObject: true
+				} );
+				model.schema.extend( '$text', { allowIn: 'img' } );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -42,12 +53,12 @@ describe( 'DeleteCommand integration', () => {
 	} );
 	} );
 
 
 	function assertOutput( output ) {
 	function assertOutput( output ) {
-		expect( getData( doc ) ).to.equal( output );
+		expect( getData( model ) ).to.equal( output );
 	}
 	}
 
 
 	describe( 'with undo', () => {
 	describe( 'with undo', () => {
 		it( 'deletes characters (and group changes in batches) and rollbacks', () => {
 		it( 'deletes characters (and group changes in batches) and rollbacks', () => {
-			setData( doc, '<paragraph>123456789[]</paragraph>' );
+			setData( model, '<paragraph>123456789[]</paragraph>' );
 
 
 			for ( let i = 0; i < 3; ++i ) {
 			for ( let i = 0; i < 3; ++i ) {
 				editor.execute( 'delete' );
 				editor.execute( 'delete' );
@@ -59,7 +70,7 @@ describe( 'DeleteCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'deletes characters (and group changes in batches) and rollbacks - test step', () => {
 		it( 'deletes characters (and group changes in batches) and rollbacks - test step', () => {
-			setData( doc, '<paragraph>123456789[]</paragraph>' );
+			setData( model, '<paragraph>123456789[]</paragraph>' );
 
 
 			for ( let i = 0; i < 6; ++i ) {
 			for ( let i = 0; i < 6; ++i ) {
 				editor.execute( 'delete' );
 				editor.execute( 'delete' );
@@ -75,7 +86,7 @@ describe( 'DeleteCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'deletes elements (and group changes in batches) and rollbacks', () => {
 		it( 'deletes elements (and group changes in batches) and rollbacks', () => {
-			setData( doc, '<paragraph><img>1</img><img>2</img><img>3</img><img>4</img><img>5</img><img>6</img>[]</paragraph>' );
+			setData( model, '<paragraph><img>1</img><img>2</img><img>3</img><img>4</img><img>5</img><img>6</img>[]</paragraph>' );
 
 
 			for ( let i = 0; i < 3; ++i ) {
 			for ( let i = 0; i < 3; ++i ) {
 				editor.execute( 'delete' );
 				editor.execute( 'delete' );
@@ -87,7 +98,7 @@ describe( 'DeleteCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'merges elements (and group changes in batches) and rollbacks', () => {
 		it( 'merges elements (and group changes in batches) and rollbacks', () => {
-			setData( doc, '<paragraph>123456</paragraph><paragraph>[]78</paragraph>' );
+			setData( model, '<paragraph>123456</paragraph><paragraph>[]78</paragraph>' );
 
 
 			for ( let i = 0; i < 6; ++i ) {
 			for ( let i = 0; i < 6; ++i ) {
 				editor.execute( 'delete' );
 				editor.execute( 'delete' );
@@ -107,7 +118,7 @@ describe( 'DeleteCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'merges elements (and group changes in batches) and rollbacks - non-collapsed selection', () => {
 		it( 'merges elements (and group changes in batches) and rollbacks - non-collapsed selection', () => {
-			setData( doc, '<paragraph>12345[6</paragraph><paragraph>7]8</paragraph>' );
+			setData( model, '<paragraph>12345[6</paragraph><paragraph>7]8</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
@@ -125,11 +136,11 @@ describe( 'DeleteCommand integration', () => {
 
 
 	describe( 'with DataController.deleteContent', () => {
 	describe( 'with DataController.deleteContent', () => {
 		beforeEach( () => {
 		beforeEach( () => {
-			doc.schema.registerItem( 'h1', '$block' );
+			model.schema.register( 'h1', { inheritAllFrom: '$block' } );
 		} );
 		} );
 
 
 		it( 'should replace content with paragraph - if whole content is selected', () => {
 		it( 'should replace content with paragraph - if whole content is selected', () => {
-			setData( doc, '<h1>[foo</h1><paragraph>bar]</paragraph>' );
+			setData( model, '<h1>[foo</h1><paragraph>bar]</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
@@ -137,7 +148,7 @@ describe( 'DeleteCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'should not replace content with paragraph - if not whole content is selected', () => {
 		it( 'should not replace content with paragraph - if not whole content is selected', () => {
-			setData( doc, '<h1>f[oo</h1><paragraph>bar]</paragraph>' );
+			setData( model, '<h1>f[oo</h1><paragraph>bar]</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
@@ -145,11 +156,53 @@ describe( 'DeleteCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'should not replace content with paragraph - if selection was collapsed', () => {
 		it( 'should not replace content with paragraph - if selection was collapsed', () => {
-			setData( doc, '<h1></h1><paragraph>[]</paragraph>' );
+			setData( model, '<h1></h1><paragraph>[]</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
 			assertOutput( '<h1>[]</h1>' );
 			assertOutput( '<h1>[]</h1>' );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'with multi-range selection', () => {
+		let element, editor, model;
+
+		beforeEach( () => {
+			element = document.createElement( 'div' );
+			document.body.appendChild( element );
+
+			return ClassicEditor
+				.create( element, {
+					plugins: [ Typing, Heading, List, Image, ImageCaption, Paragraph, BlockQuote ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+					model = editor.model;
+				} );
+		} );
+
+		afterEach( () => {
+			element.remove();
+
+			return editor.destroy();
+		} );
+
+		it( 'should not throw an error if content with the image is being removed', () => {
+			setData( model,
+				'<listItem indent="0" type="numbered">OL List i[tem 1</listItem>' +
+				'<listItem indent="0" type="numbered">OL List item 2</listItem>]' +
+				'<image alt="bar" imageStyle="side" src="sample.jpg"><caption>[Caption</caption></image>' +
+				'<blockQuote>' +
+					'<paragraph>Quote</paragraph>' +
+					'<listItem indent="0" type="bulleted">Quoted UL List item 1</listItem>' +
+					'<listItem indent="0" type="bulleted">Quote]d UL List item 2</listItem>' +
+					'<paragraph>Quote</paragraph>' +
+				'</blockQuote>'
+			);
+
+			expect( () => {
+				editor.execute( 'delete' );
+			} ).to.not.throw();
+		} );
+	} );
 } );
 } );

+ 69 - 56
packages/ckeditor5-typing/tests/deletecommand.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -9,7 +9,7 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 
 describe( 'DeleteCommand', () => {
 describe( 'DeleteCommand', () => {
-	let editor, doc;
+	let editor, model, doc;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
@@ -17,13 +17,14 @@ describe( 'DeleteCommand', () => {
 		return ModelTestEditor.create()
 		return ModelTestEditor.create()
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
-				doc = editor.document;
+				model = editor.model;
+				doc = model.document;
 
 
 				const command = new DeleteCommand( editor, 'backward' );
 				const command = new DeleteCommand( editor, 'backward' );
 				editor.commands.add( 'delete', command );
 				editor.commands.add( 'delete', command );
 
 
-				doc.schema.registerItem( 'paragraph', '$block' );
-				doc.schema.registerItem( 'heading1', '$block' );
+				model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+				model.schema.register( 'heading1', { inheritAllFrom: '$block' } );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -38,23 +39,23 @@ describe( 'DeleteCommand', () => {
 	} );
 	} );
 
 
 	describe( 'execute()', () => {
 	describe( 'execute()', () => {
-		it( 'uses enqueueChanges', () => {
-			setData( doc, '<paragraph>foo[]bar</paragraph>' );
+		it( 'uses enqueueChange', () => {
+			setData( model, '<paragraph>foo[]bar</paragraph>' );
 
 
-			doc.enqueueChanges( () => {
+			model.enqueueChange( () => {
 				editor.execute( 'delete' );
 				editor.execute( 'delete' );
 
 
 				// We expect that command is executed in enqueue changes block. Since we are already in
 				// We expect that command is executed in enqueue changes block. Since we are already in
 				// an enqueued block, the command execution will be postponed. Hence, no changes.
 				// an enqueued block, the command execution will be postponed. Hence, no changes.
-				expect( getData( doc ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
+				expect( getData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 			} );
 			} );
 
 
 			// After all enqueued changes are done, the command execution is reflected.
 			// After all enqueued changes are done, the command execution is reflected.
-			expect( getData( doc ) ).to.equal( '<paragraph>fo[]bar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>fo[]bar</paragraph>' );
 		} );
 		} );
 
 
 		it( 'locks buffer when executing', () => {
 		it( 'locks buffer when executing', () => {
-			setData( doc, '<paragraph>foo[]bar</paragraph>' );
+			setData( model, '<paragraph>foo[]bar</paragraph>' );
 
 
 			const buffer = editor.commands.get( 'delete' )._buffer;
 			const buffer = editor.commands.get( 'delete' )._buffer;
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
@@ -67,46 +68,46 @@ describe( 'DeleteCommand', () => {
 		} );
 		} );
 
 
 		it( 'deletes previous character when selection is collapsed', () => {
 		it( 'deletes previous character when selection is collapsed', () => {
-			setData( doc, '<paragraph>foo[]bar</paragraph>' );
+			setData( model, '<paragraph>foo[]bar</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<paragraph>fo[]bar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>fo[]bar</paragraph>' );
 		} );
 		} );
 
 
 		it( 'deletes selection contents', () => {
 		it( 'deletes selection contents', () => {
-			setData( doc, '<paragraph>fo[ob]ar</paragraph>' );
+			setData( model, '<paragraph>fo[ob]ar</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
 		} );
 		} );
 
 
 		it( 'merges elements', () => {
 		it( 'merges elements', () => {
-			setData( doc, '<paragraph>foo</paragraph><paragraph>[]bar</paragraph>' );
+			setData( model, '<paragraph>foo</paragraph><paragraph>[]bar</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 		} );
 		} );
 
 
 		it( 'does not try to delete when selection is at the boundary', () => {
 		it( 'does not try to delete when selection is at the boundary', () => {
 			const spy = sinon.spy();
 			const spy = sinon.spy();
 
 
-			editor.data.on( 'deleteContent', spy );
-			setData( doc, '<paragraph>[]foo</paragraph>' );
+			editor.model.on( 'deleteContent', spy );
+			setData( model, '<paragraph>[]foo</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<paragraph>[]foo</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>[]foo</paragraph>' );
 			expect( spy.callCount ).to.equal( 0 );
 			expect( spy.callCount ).to.equal( 0 );
 		} );
 		} );
 
 
 		it( 'passes options to modifySelection', () => {
 		it( 'passes options to modifySelection', () => {
 			const spy = sinon.spy();
 			const spy = sinon.spy();
 
 
-			editor.data.on( 'modifySelection', spy );
-			setData( doc, '<paragraph>foo[]bar</paragraph>' );
+			editor.model.on( 'modifySelection', spy );
+			setData( model, '<paragraph>foo[]bar</paragraph>' );
 
 
 			editor.commands.get( 'delete' ).direction = 'forward';
 			editor.commands.get( 'delete' ).direction = 'forward';
 
 
@@ -122,45 +123,47 @@ describe( 'DeleteCommand', () => {
 		it( 'passes options to deleteContent #1', () => {
 		it( 'passes options to deleteContent #1', () => {
 			const spy = sinon.spy();
 			const spy = sinon.spy();
 
 
-			editor.data.on( 'deleteContent', spy );
-			setData( doc, '<paragraph>foo[]bar</paragraph>' );
+			editor.model.on( 'deleteContent', spy );
+			setData( model, '<paragraph>foo[]bar</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
 			expect( spy.callCount ).to.equal( 1 );
 			expect( spy.callCount ).to.equal( 1 );
 
 
-			const deleteOpts = spy.args[ 0 ][ 1 ][ 2 ];
+			const deleteOpts = spy.args[ 0 ][ 1 ][ 1 ];
 			expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', true );
 			expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', true );
 		} );
 		} );
 
 
 		it( 'passes options to deleteContent #2', () => {
 		it( 'passes options to deleteContent #2', () => {
 			const spy = sinon.spy();
 			const spy = sinon.spy();
 
 
-			editor.data.on( 'deleteContent', spy );
-			setData( doc, '<paragraph>[foobar]</paragraph>' );
+			editor.model.on( 'deleteContent', spy );
+			setData( model, '<paragraph>[foobar]</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
 			expect( spy.callCount ).to.equal( 1 );
 			expect( spy.callCount ).to.equal( 1 );
 
 
-			const deleteOpts = spy.args[ 0 ][ 1 ][ 2 ];
+			const deleteOpts = spy.args[ 0 ][ 1 ][ 1 ];
 			expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', false );
 			expect( deleteOpts ).to.have.property( 'doNotResetEntireContent', false );
 		} );
 		} );
 
 
 		it( 'leaves an empty paragraph after removing the whole content from editor', () => {
 		it( 'leaves an empty paragraph after removing the whole content from editor', () => {
-			setData( doc, '<heading1>[Header 1</heading1><paragraph>Some text.]</paragraph>' );
+			setData( model, '<heading1>[Header 1</heading1><paragraph>Some text.]</paragraph>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<paragraph>[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 		} );
 		} );
 
 
 		it( 'leaves an empty paragraph after removing the whole content inside limit element', () => {
 		it( 'leaves an empty paragraph after removing the whole content inside limit element', () => {
-			doc.schema.registerItem( 'section', '$root' );
-			doc.schema.limits.add( 'section' );
-			doc.schema.allow( { name: 'section', inside: '$root' } );
+			model.schema.register( 'section', {
+				inheritAllFrom: '$root',
+				allowIn: '$root',
+				isLimit: true
+			} );
 
 
-			setData( doc,
+			setData( model,
 				'<heading1>Foo</heading1>' +
 				'<heading1>Foo</heading1>' +
 					'<section>' +
 					'<section>' +
 						'<heading1>[Header 1</heading1>' +
 						'<heading1>[Header 1</heading1>' +
@@ -171,7 +174,7 @@ describe( 'DeleteCommand', () => {
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal(
+			expect( getData( model ) ).to.equal(
 				'<heading1>Foo</heading1>' +
 				'<heading1>Foo</heading1>' +
 				'<section>' +
 				'<section>' +
 					'<paragraph>[]</paragraph>' +
 					'<paragraph>[]</paragraph>' +
@@ -181,51 +184,57 @@ describe( 'DeleteCommand', () => {
 		} );
 		} );
 
 
 		it( 'leaves an empty paragraph after removing another paragraph from block element', () => {
 		it( 'leaves an empty paragraph after removing another paragraph from block element', () => {
-			doc.schema.registerItem( 'section', '$block' );
-			doc.schema.registerItem( 'blockQuote', '$block' );
-			doc.schema.limits.add( 'section' );
-			doc.schema.allow( { name: 'section', inside: '$root' } );
-			doc.schema.allow( { name: 'paragraph', inside: 'section' } );
-			doc.schema.allow( { name: 'blockQuote', inside: 'section' } );
-			doc.schema.allow( { name: 'paragraph', inside: 'blockQuote' } );
+			model.schema.register( 'section', {
+				inheritAllFrom: '$block',
+				isLimit: true
+			} );
+			model.schema.register( 'blockQuote', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'section', { allowIn: '$root' } );
+			model.schema.extend( 'paragraph', { allowIn: 'section' } );
+			model.schema.extend( 'blockQuote', { allowIn: 'section' } );
+			model.schema.extend( 'paragraph', { allowIn: 'blockQuote' } );
 
 
-			setData( doc, '<section><blockQuote><paragraph>[]</paragraph></blockQuote></section>' );
+			setData( model, '<section><blockQuote><paragraph>[]</paragraph></blockQuote></section>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<section><paragraph>[]</paragraph></section>' );
+			expect( getData( model ) ).to.equal( '<section><paragraph>[]</paragraph></section>' );
 		} );
 		} );
 
 
-		it( 'leaves an empty paragraph after removing the whole content when root element was not added as Schema.limits', () => {
-			doc.schema.limits.delete( '$root' );
+		it( 'leaves an empty paragraph after removing the whole content when root element was not added as Schema limit', () => {
+			model.schema.extend( '$root', {
+				isLimit: false
+			} );
 
 
-			setData( doc, '<heading1>[]</heading1>' );
+			expect( model.schema.isLimit( '$root' ) ).to.be.false;
+
+			setData( model, '<heading1>[]</heading1>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<paragraph>[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 		} );
 		} );
 
 
 		it( 'replaces an empty element with paragraph', () => {
 		it( 'replaces an empty element with paragraph', () => {
-			setData( doc, '<heading1>[]</heading1>' );
+			setData( model, '<heading1>[]</heading1>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<paragraph>[]</paragraph>' );
+			expect( getData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
 		} );
 		} );
 
 
 		it( 'does not replace an element when Backspace or Delete key is held', () => {
 		it( 'does not replace an element when Backspace or Delete key is held', () => {
-			setData( doc, '<heading1>Bar[]</heading1>' );
+			setData( model, '<heading1>Bar[]</heading1>' );
 
 
 			for ( let sequence = 1; sequence < 10; ++sequence ) {
 			for ( let sequence = 1; sequence < 10; ++sequence ) {
 				editor.execute( 'delete', { sequence } );
 				editor.execute( 'delete', { sequence } );
 			}
 			}
 
 
-			expect( getData( doc ) ).to.equal( '<heading1>[]</heading1>' );
+			expect( getData( model ) ).to.equal( '<heading1>[]</heading1>' );
 		} );
 		} );
 
 
 		it( 'does not replace with paragraph in another paragraph already occurs in limit element', () => {
 		it( 'does not replace with paragraph in another paragraph already occurs in limit element', () => {
-			setData( doc, '<paragraph>[]</paragraph>' );
+			setData( model, '<paragraph>[]</paragraph>' );
 
 
 			const element = doc.getRoot().getNodeByPath( [ 0 ] );
 			const element = doc.getRoot().getNodeByPath( [ 0 ] );
 
 
@@ -235,13 +244,17 @@ describe( 'DeleteCommand', () => {
 		} );
 		} );
 
 
 		it( 'does not replace an element if a paragraph is not allowed in current position', () => {
 		it( 'does not replace an element if a paragraph is not allowed in current position', () => {
-			doc.schema.disallow( { name: 'paragraph', inside: '$root' } );
+			model.schema.addChildCheck( ( ctx, childDef ) => {
+				if ( ctx.endsWith( '$root' ) && childDef.name == 'paragraph' ) {
+					return false;
+				}
+			} );
 
 
-			setData( doc, '<heading1>[]</heading1>' );
+			setData( model, '<heading1>[]</heading1>' );
 
 
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 
 
-			expect( getData( doc ) ).to.equal( '<heading1>[]</heading1>' );
+			expect( getData( model ) ).to.equal( '<heading1>[]</heading1>' );
 		} );
 		} );
 	} );
 	} );
 } );
 } );

+ 65 - 7
packages/ckeditor5-typing/tests/deleteobserver.js

@@ -1,27 +1,39 @@
 /**
 /**
- * @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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
 /* globals document */
 /* globals document */
 
 
 import DeleteObserver from '../src/deleteobserver';
 import DeleteObserver from '../src/deleteobserver';
-import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
+import View from '@ckeditor/ckeditor5-engine/src/view/view';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
+import createViewRoot from '@ckeditor/ckeditor5-engine/tests/view/_utils/createroot';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import env from '@ckeditor/ckeditor5-utils/src/env';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+testUtils.createSinonSandbox();
 
 
 describe( 'DeleteObserver', () => {
 describe( 'DeleteObserver', () => {
-	let viewDocument;
+	let view, viewDocument;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
-		viewDocument.addObserver( DeleteObserver );
+		view = new View();
+		viewDocument = view.document;
+		view.addObserver( DeleteObserver );
+	} );
+
+	afterEach( () => {
+		view.destroy();
 	} );
 	} );
 
 
 	// See ckeditor/ckeditor5-enter#10.
 	// See ckeditor/ckeditor5-enter#10.
 	it( 'can be initialized', () => {
 	it( 'can be initialized', () => {
 		expect( () => {
 		expect( () => {
-			viewDocument.createRoot( document.createElement( 'div' ) );
+			createViewRoot( viewDocument );
+			view.attachDomRoot( document.createElement( 'div' ) );
 		} ).to.not.throw();
 		} ).to.not.throw();
 	} );
 	} );
 
 
@@ -43,9 +55,11 @@ describe( 'DeleteObserver', () => {
 			expect( data ).to.have.property( 'sequence', 1 );
 			expect( data ).to.have.property( 'sequence', 1 );
 		} );
 		} );
 
 
-		it( 'is fired with a proper direction and unit', () => {
+		it( 'is fired with a proper direction and unit (on Mac)', () => {
 			const spy = sinon.spy();
 			const spy = sinon.spy();
 
 
+			testUtils.sinon.stub( env, 'isMac' ).value( true );
+
 			viewDocument.on( 'delete', spy );
 			viewDocument.on( 'delete', spy );
 
 
 			viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
 			viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
@@ -61,6 +75,26 @@ describe( 'DeleteObserver', () => {
 			expect( data ).to.have.property( 'sequence', 1 );
 			expect( data ).to.have.property( 'sequence', 1 );
 		} );
 		} );
 
 
+		it( 'is fired with a proper direction and unit (on non-Mac)', () => {
+			const spy = sinon.spy();
+
+			testUtils.sinon.stub( env, 'isMac' ).value( false );
+
+			viewDocument.on( 'delete', spy );
+
+			viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
+				keyCode: getCode( 'backspace' ),
+				ctrlKey: true
+			} ) );
+
+			expect( spy.calledOnce ).to.be.true;
+
+			const data = spy.args[ 0 ][ 1 ];
+			expect( data ).to.have.property( 'direction', 'backward' );
+			expect( data ).to.have.property( 'unit', 'word' );
+			expect( data ).to.have.property( 'sequence', 1 );
+		} );
+
 		it( 'is not fired on keydown when keyCode does not match backspace or delete', () => {
 		it( 'is not fired on keydown when keyCode does not match backspace or delete', () => {
 			const spy = sinon.spy();
 			const spy = sinon.spy();
 
 
@@ -167,6 +201,30 @@ describe( 'DeleteObserver', () => {
 			expect( spy.args[ 0 ][ 1 ] ).to.have.property( 'sequence', 1 );
 			expect( spy.args[ 0 ][ 1 ] ).to.have.property( 'sequence', 1 );
 			expect( spy.args[ 1 ][ 1 ] ).to.have.property( 'sequence', 2 );
 			expect( spy.args[ 1 ][ 1 ] ).to.have.property( 'sequence', 2 );
 		} );
 		} );
+
+		it( 'should stop keydown event when delete event is stopped', () => {
+			const keydownSpy = sinon.spy();
+			viewDocument.on( 'keydown', keydownSpy );
+			viewDocument.on( 'delete', evt => evt.stop() );
+
+			viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
+				keyCode: getCode( 'delete' )
+			} ) );
+
+			sinon.assert.notCalled( keydownSpy );
+		} );
+
+		it( 'should not stop keydown event when delete event is not stopped', () => {
+			const keydownSpy = sinon.spy();
+			viewDocument.on( 'keydown', keydownSpy );
+			viewDocument.on( 'delete', evt => evt.stop() );
+
+			viewDocument.fire( 'keydown', new DomEventData( viewDocument, getDomEvent(), {
+				keyCode: getCode( 'x' )
+			} ) );
+
+			sinon.assert.calledOnce( keydownSpy );
+		} );
 	} );
 	} );
 
 
 	function getDomEvent() {
 	function getDomEvent() {

+ 151 - 540
packages/ckeditor5-typing/tests/input.js

@@ -1,29 +1,21 @@
-/*
- * @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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* global document */
-
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import Bold from '@ckeditor/ckeditor5-basic-styles/src/boldengine';
-import Italic from '@ckeditor/ckeditor5-basic-styles/src/italicengine';
-import LinkEngine from '@ckeditor/ckeditor5-link/src/linkengine';
+import List from '@ckeditor/ckeditor5-list/src/list';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Input from '../src/input';
 import Input from '../src/input';
 
 
-import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
+import Writer from '@ckeditor/ckeditor5-engine/src/model/writer';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
-import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter';
-import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter';
 
 
 import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
 import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
 import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
-import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
 import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection';
 import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection';
-import MutationObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mutationobserver';
 
 
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
@@ -31,35 +23,35 @@ import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 
+/* global document */
+
 describe( 'Input feature', () => {
 describe( 'Input feature', () => {
-	let editor, model, modelRoot, view, viewRoot, listenter;
+	let editor, model, modelRoot, view, viewDocument, viewRoot, listenter;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
-	before( () => {
+	beforeEach( () => {
 		listenter = Object.create( EmitterMixin );
 		listenter = Object.create( EmitterMixin );
 
 
-		return VirtualTestEditor
-			.create( {
-				plugins: [ Input, Paragraph, Bold ]
-			} )
+		const domElement = document.createElement( 'div' );
+		document.body.appendChild( domElement );
+
+		return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, List ] } )
 			.then( newEditor => {
 			.then( newEditor => {
 				// Mock image feature.
 				// Mock image feature.
-				newEditor.document.schema.registerItem( 'image', '$inline' );
+				newEditor.model.schema.register( 'image', { allowWhere: '$text' } );
 
 
-				buildModelConverter().for( newEditor.data.modelToView, newEditor.editing.modelToView )
-					.fromElement( 'image' )
-					.toElement( 'img' );
-
-				buildViewConverter().for( newEditor.data.viewToModel )
-					.fromElement( 'img' )
-					.toElement( 'image' );
+				newEditor.conversion.elementToElement( {
+					model: 'image',
+					view: 'img'
+				} );
 
 
 				editor = newEditor;
 				editor = newEditor;
-				model = editor.editing.model;
-				modelRoot = model.getRoot();
+				model = editor.model;
+				modelRoot = model.document.getRoot();
 				view = editor.editing.view;
 				view = editor.editing.view;
-				viewRoot = view.getRoot();
+				viewDocument = view.document;
+				viewRoot = viewDocument.getRoot();
 			} );
 			} );
 	} );
 	} );
 
 
@@ -70,10 +62,10 @@ describe( 'Input feature', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		editor.setData( '<p>foobar</p>' );
 		editor.setData( '<p>foobar</p>' );
 
 
-		model.enqueueChanges( () => {
-			model.selection.setRanges( [
+		model.change( writer => {
+			writer.setSelection(
 				ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 3, modelRoot.getChild( 0 ), 3 )
 				ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 3, modelRoot.getChild( 0 ), 3 )
-			] );
+			);
 		} );
 		} );
 	} );
 	} );
 
 
@@ -83,7 +75,7 @@ describe( 'Input feature', () => {
 
 
 	describe( 'mutations handling', () => {
 	describe( 'mutations handling', () => {
 		it( 'should handle text mutation', () => {
 		it( 'should handle text mutation', () => {
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'text',
 					type: 'text',
 					oldText: 'foobar',
 					oldText: 'foobar',
@@ -97,7 +89,7 @@ describe( 'Input feature', () => {
 		} );
 		} );
 
 
 		it( 'should handle text mutation change', () => {
 		it( 'should handle text mutation change', () => {
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'text',
 					type: 'text',
 					oldText: 'foobar',
 					oldText: 'foobar',
@@ -113,7 +105,7 @@ describe( 'Input feature', () => {
 		it( 'should handle text node insertion', () => {
 		it( 'should handle text node insertion', () => {
 			editor.setData( '<p></p>' );
 			editor.setData( '<p></p>' );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'children',
 					type: 'children',
 					oldChildren: [],
 					oldChildren: [],
@@ -126,10 +118,31 @@ describe( 'Input feature', () => {
 			expect( getViewData( view ) ).to.equal( '<p>x{}</p>' );
 			expect( getViewData( view ) ).to.equal( '<p>x{}</p>' );
 		} );
 		} );
 
 
+		it( 'should apply selection attributes to the inserted text', () => {
+			setModelData( model, '<paragraph>[]</paragraph>', {
+				selectionAttributes: {
+					bold: true,
+					italic: true
+				}
+			} );
+			viewDocument.fire( 'mutations', [
+				{
+					type: 'children',
+					oldChildren: [],
+					newChildren: [ new ViewText( 'x' ) ],
+					node: viewRoot.getChild( 0 )
+				}
+			] );
+
+			expect( getModelData( model ) ).to.equal(
+				'<paragraph><$text bold="true" italic="true">x[]</$text></paragraph>'
+			);
+		} );
+
 		it( 'should handle multiple text mutations', () => {
 		it( 'should handle multiple text mutations', () => {
 			editor.setData( '<p>foo<strong>bar</strong></p>' );
 			editor.setData( '<p>foo<strong>bar</strong></p>' );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'text',
 					type: 'text',
 					oldText: 'foo',
 					oldText: 'foo',
@@ -151,7 +164,7 @@ describe( 'Input feature', () => {
 		it( 'should handle multiple text node insertion', () => {
 		it( 'should handle multiple text node insertion', () => {
 			editor.setData( '<p></p><p></p>' );
 			editor.setData( '<p></p><p></p>' );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'children',
 					type: 'children',
 					oldChildren: [],
 					oldChildren: [],
@@ -173,7 +186,7 @@ describe( 'Input feature', () => {
 		it( 'should do nothing when two nodes were inserted', () => {
 		it( 'should do nothing when two nodes were inserted', () => {
 			editor.setData( '<p></p>' );
 			editor.setData( '<p></p>' );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'children',
 					type: 'children',
 					oldChildren: [],
 					oldChildren: [],
@@ -187,7 +200,7 @@ describe( 'Input feature', () => {
 		} );
 		} );
 
 
 		it( 'should do nothing when two nodes were inserted and one removed', () => {
 		it( 'should do nothing when two nodes were inserted and one removed', () => {
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'children',
 					type: 'children',
 					oldChildren: [ new ViewText( 'foobar' ) ],
 					oldChildren: [ new ViewText( 'foobar' ) ],
@@ -203,7 +216,7 @@ describe( 'Input feature', () => {
 		it( 'should handle multiple children in the node', () => {
 		it( 'should handle multiple children in the node', () => {
 			editor.setData( '<p>foo<img></img></p>' );
 			editor.setData( '<p>foo<img></img></p>' );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'children',
 					type: 'children',
 					oldChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ) ],
 					oldChildren: [ new ViewText( 'foo' ), viewRoot.getChild( 0 ).getChild( 1 ) ],
@@ -217,7 +230,7 @@ describe( 'Input feature', () => {
 		} );
 		} );
 
 
 		it( 'should do nothing when node was removed', () => {
 		it( 'should do nothing when node was removed', () => {
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'children',
 					type: 'children',
 					oldChildren: [ new ViewText( 'foobar' ) ],
 					oldChildren: [ new ViewText( 'foobar' ) ],
@@ -233,7 +246,7 @@ describe( 'Input feature', () => {
 		it( 'should do nothing when element was inserted', () => {
 		it( 'should do nothing when element was inserted', () => {
 			editor.setData( '<p></p>' );
 			editor.setData( '<p></p>' );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'children',
 					type: 'children',
 					oldChildren: [],
 					oldChildren: [],
@@ -250,9 +263,9 @@ describe( 'Input feature', () => {
 			// This test case emulates spellchecker correction.
 			// This test case emulates spellchecker correction.
 
 
 			const viewSelection = new ViewSelection();
 			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
+			viewSelection._setTo( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
 
 
-			view.fire( 'mutations',
+			viewDocument.fire( 'mutations',
 				[ {
 				[ {
 					type: 'text',
 					type: 'text',
 					oldText: 'foobar',
 					oldText: 'foobar',
@@ -270,12 +283,12 @@ describe( 'Input feature', () => {
 			// This test case emulates spellchecker correction.
 			// This test case emulates spellchecker correction.
 
 
 			const viewSelection = new ViewSelection();
 			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
+			viewSelection._setTo( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
 
 
-			testUtils.sinon.spy( Batch.prototype, 'weakInsert' );
-			testUtils.sinon.spy( Batch.prototype, 'remove' );
+			testUtils.sinon.spy( Writer.prototype, 'insert' );
+			testUtils.sinon.spy( Writer.prototype, 'remove' );
 
 
-			view.fire( 'mutations',
+			viewDocument.fire( 'mutations',
 				[ {
 				[ {
 					type: 'text',
 					type: 'text',
 					oldText: 'foobar',
 					oldText: 'foobar',
@@ -285,8 +298,8 @@ describe( 'Input feature', () => {
 				viewSelection
 				viewSelection
 			);
 			);
 
 
-			expect( Batch.prototype.weakInsert.calledOnce ).to.be.true;
-			expect( Batch.prototype.remove.calledOnce ).to.be.true;
+			expect( Writer.prototype.insert.calledOnce ).to.be.true;
+			expect( Writer.prototype.remove.calledOnce ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should place selection after when correcting to longer word (spellchecker)', () => {
 		it( 'should place selection after when correcting to longer word (spellchecker)', () => {
@@ -294,9 +307,9 @@ describe( 'Input feature', () => {
 			editor.setData( '<p>Foo hous a</p>' );
 			editor.setData( '<p>Foo hous a</p>' );
 
 
 			const viewSelection = new ViewSelection();
 			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
+			viewSelection._setTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
 
 
-			view.fire( 'mutations',
+			viewDocument.fire( 'mutations',
 				[ {
 				[ {
 					type: 'text',
 					type: 'text',
 					oldText: 'Foo hous a',
 					oldText: 'Foo hous a',
@@ -315,9 +328,9 @@ describe( 'Input feature', () => {
 			editor.setData( '<p>Bar athat foo</p>' );
 			editor.setData( '<p>Bar athat foo</p>' );
 
 
 			const viewSelection = new ViewSelection();
 			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
+			viewSelection._setTo( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
 
 
-			view.fire( 'mutations',
+			viewDocument.fire( 'mutations',
 				[ {
 				[ {
 					type: 'text',
 					type: 'text',
 					oldText: 'Bar athat foo',
 					oldText: 'Bar athat foo',
@@ -336,9 +349,9 @@ describe( 'Input feature', () => {
 			editor.setData( '<p>Foo hous e</p>' );
 			editor.setData( '<p>Foo hous e</p>' );
 
 
 			const viewSelection = new ViewSelection();
 			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
+			viewSelection._setTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
 
 
-			view.fire( 'mutations',
+			viewDocument.fire( 'mutations',
 				[ {
 				[ {
 					type: 'text',
 					type: 'text',
 					oldText: 'Foo hous e',
 					oldText: 'Foo hous e',
@@ -356,10 +369,10 @@ describe( 'Input feature', () => {
 			editor.setData( '<p>Foo house</p>' );
 			editor.setData( '<p>Foo house</p>' );
 
 
 			const viewSelection = new ViewSelection();
 			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
-			viewSelection.moveFocusTo( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
+			viewSelection._setTo( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
+			viewSelection._setFocus( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
 
 
-			view.fire( 'mutations',
+			viewDocument.fire( 'mutations',
 				[ {
 				[ {
 					type: 'text',
 					type: 'text',
 					oldText: 'Foo house',
 					oldText: 'Foo house',
@@ -374,13 +387,13 @@ describe( 'Input feature', () => {
 		} );
 		} );
 
 
 		it( 'should replace last &nbsp; with space', () => {
 		it( 'should replace last &nbsp; with space', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
+			model.change( writer => {
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
-				] );
+				);
 			} );
 			} );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'text',
 					type: 'text',
 					oldText: 'foobar',
 					oldText: 'foobar',
@@ -394,13 +407,13 @@ describe( 'Input feature', () => {
 		} );
 		} );
 
 
 		it( 'should replace first &nbsp; with space', () => {
 		it( 'should replace first &nbsp; with space', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
+			model.change( writer => {
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 0, modelRoot.getChild( 0 ), 0 )
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 0, modelRoot.getChild( 0 ), 0 )
-				] );
+				);
 			} );
 			} );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'text',
 					type: 'text',
 					oldText: 'foobar',
 					oldText: 'foobar',
@@ -414,13 +427,13 @@ describe( 'Input feature', () => {
 		} );
 		} );
 
 
 		it( 'should replace all &nbsp; with spaces', () => {
 		it( 'should replace all &nbsp; with spaces', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
+			model.change( writer => {
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
-				] );
+				);
 			} );
 			} );
 
 
-			view.fire( 'mutations', [
+			viewDocument.fire( 'mutations', [
 				{
 				{
 					type: 'text',
 					type: 'text',
 					oldText: 'foobar',
 					oldText: 'foobar',
@@ -432,16 +445,40 @@ describe( 'Input feature', () => {
 			expect( getModelData( model ) ).to.equal( '<paragraph>foobar   baz[]</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foobar   baz[]</paragraph>' );
 			expect( getViewData( view ) ).to.equal( '<p>foobar   baz{}</p>' );
 			expect( getViewData( view ) ).to.equal( '<p>foobar   baz{}</p>' );
 		} );
 		} );
+
+		// ckeditor5#718.
+		it( 'should not crash and prevent all changes if view common ancestor of mutations cannot be mapped to model', () => {
+			editor.setData( '<p>Foo</p><ul><li>Bar</li><li>Baz</li></ul>' );
+
+			const ul = viewRoot.getChild( 1 );
+
+			viewDocument.fire( 'mutations', [
+				{
+					type: 'text',
+					oldText: 'Bar',
+					newText: 'Bx',
+					node: ul.getChild( 0 )
+				},
+				{
+					type: 'children',
+					oldChildren: [ ul.getChild( 0 ), ul.getChild( 1 ) ],
+					newChildren: [ ul.getChild( 0 ) ],
+					node: ul
+				}
+			] );
+
+			expect( getViewData( view ) ).to.equal( '<p>{}Foo</p><ul><li>Bar</li><li>Baz</li></ul>' );
+		} );
 	} );
 	} );
 
 
 	describe( 'keystroke handling', () => {
 	describe( 'keystroke handling', () => {
 		it( 'should remove contents', () => {
 		it( 'should remove contents', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
+			model.change( writer => {
+				writer.setSelection(
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) );
 			} );
 			} );
 
 
-			listenter.listenTo( view, 'keydown', () => {
+			listenter.listenTo( viewDocument, 'keydown', () => {
 				expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
 			}, { priority: 'lowest' } );
 			}, { priority: 'lowest' } );
 		} );
 		} );
@@ -450,10 +487,10 @@ describe( 'Input feature', () => {
 		it( 'should remove contents and merge blocks', () => {
 		it( 'should remove contents and merge blocks', () => {
 			setModelData( model, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
 			setModelData( model, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
 
 
-			listenter.listenTo( view, 'keydown', () => {
+			listenter.listenTo( viewDocument, 'keydown', () => {
 				expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
 				expect( getModelData( model ) ).to.equal( '<paragraph>fo[]ar</paragraph>' );
 
 
-				view.fire( 'mutations', [
+				viewDocument.fire( 'mutations', [
 					{
 					{
 						type: 'text',
 						type: 'text',
 						oldText: 'foar',
 						oldText: 'foar',
@@ -463,73 +500,73 @@ describe( 'Input feature', () => {
 				] );
 				] );
 			}, { priority: 'lowest' } );
 			}, { priority: 'lowest' } );
 
 
-			view.fire( 'keydown', { keyCode: getCode( 'y' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foy[]ar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foy[]ar</paragraph>' );
 			expect( getViewData( view ) ).to.equal( '<p>foy{}ar</p>' );
 			expect( getViewData( view ) ).to.equal( '<p>foy{}ar</p>' );
 		} );
 		} );
 
 
 		it( 'should do nothing on arrow key', () => {
 		it( 'should do nothing on arrow key', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
+			model.change( writer => {
+				writer.setSelection(
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) );
 			} );
 			} );
 
 
-			view.fire( 'keydown', { keyCode: getCode( 'arrowdown' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'arrowdown' ) } );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 		} );
 		} );
 
 
 		it( 'should do nothing on ctrl combinations', () => {
 		it( 'should do nothing on ctrl combinations', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
+			model.change( writer => {
+				writer.setSelection(
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) );
 			} );
 			} );
 
 
-			view.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
+			viewDocument.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 		} );
 		} );
 
 
 		it( 'should do nothing on non printable keys', () => {
 		it( 'should do nothing on non printable keys', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
+			model.change( writer => {
+				writer.setSelection(
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) );
 			} );
 			} );
 
 
-			view.fire( 'keydown', { keyCode: 16 } ); // Shift
-			view.fire( 'keydown', { keyCode: 35 } ); // Home
-			view.fire( 'keydown', { keyCode: 112 } ); // F1
+			viewDocument.fire( 'keydown', { keyCode: 16 } ); // Shift
+			viewDocument.fire( 'keydown', { keyCode: 35 } ); // Home
+			viewDocument.fire( 'keydown', { keyCode: 112 } ); // F1
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 		} );
 		} );
 
 
 		// #69
 		// #69
 		it( 'should do nothing on tab key', () => {
 		it( 'should do nothing on tab key', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
+			model.change( writer => {
+				writer.setSelection(
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) );
 			} );
 			} );
 
 
-			view.fire( 'keydown', { keyCode: 9 } ); // Tab
+			viewDocument.fire( 'keydown', { keyCode: 9 } ); // Tab
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 		} );
 		} );
 
 
 		// #82
 		// #82
 		it( 'should do nothing on composition start key', () => {
 		it( 'should do nothing on composition start key', () => {
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
+			model.change( writer => {
+				writer.setSelection(
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) );
 			} );
 			} );
 
 
-			view.fire( 'keydown', { keyCode: 229 } );
+			viewDocument.fire( 'keydown', { keyCode: 229 } );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 		} );
 		} );
 
 
 		it( 'should do nothing if selection is collapsed', () => {
 		it( 'should do nothing if selection is collapsed', () => {
-			view.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
+			viewDocument.fire( 'keydown', { ctrlKey: true, keyCode: getCode( 'c' ) } );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 		} );
 		} );
@@ -539,12 +576,12 @@ describe( 'Input feature', () => {
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
 			const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
 			const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
 
 
-			model.enqueueChanges( () => {
-				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) ] );
+			model.change( writer => {
+				writer.setSelection(
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 2, modelRoot.getChild( 0 ), 4 ) );
 			} );
 			} );
 
 
-			view.fire( 'keydown', { keyCode: getCode( 'y' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'y' ) } );
 
 
 			expect( lockSpy.calledOnce ).to.be.true;
 			expect( lockSpy.calledOnce ).to.be.true;
 			expect( unlockSpy.calledOnce ).to.be.true;
 			expect( unlockSpy.calledOnce ).to.be.true;
@@ -555,9 +592,9 @@ describe( 'Input feature', () => {
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
 			const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
 			const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
 
 
-			view.fire( 'keydown', { keyCode: 16 } ); // Shift
-			view.fire( 'keydown', { keyCode: 35 } ); // Home
-			view.fire( 'keydown', { keyCode: 112 } ); // F1
+			viewDocument.fire( 'keydown', { keyCode: 16 } ); // Shift
+			viewDocument.fire( 'keydown', { keyCode: 35 } ); // Home
+			viewDocument.fire( 'keydown', { keyCode: 112 } ); // F1
 
 
 			expect( lockSpy.callCount ).to.be.equal( 0 );
 			expect( lockSpy.callCount ).to.be.equal( 0 );
 			expect( unlockSpy.callCount ).to.be.equal( 0 );
 			expect( unlockSpy.callCount ).to.be.equal( 0 );
@@ -568,9 +605,9 @@ describe( 'Input feature', () => {
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
 			const lockSpy = testUtils.sinon.spy( buffer, 'lock' );
 			const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
 			const unlockSpy = testUtils.sinon.spy( buffer, 'unlock' );
 
 
-			view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
-			view.fire( 'keydown', { keyCode: getCode( 'a' ) } );
-			view.fire( 'keydown', { keyCode: getCode( 'z' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'a' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'z' ) } );
 
 
 			expect( lockSpy.callCount ).to.be.equal( 0 );
 			expect( lockSpy.callCount ).to.be.equal( 0 );
 			expect( unlockSpy.callCount ).to.be.equal( 0 );
 			expect( unlockSpy.callCount ).to.be.equal( 0 );
@@ -581,7 +618,7 @@ describe( 'Input feature', () => {
 
 
 			editor.commands.get( 'input' ).isEnabled = false;
 			editor.commands.get( 'input' ).isEnabled = false;
 
 
-			view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 		} );
 		} );
@@ -591,436 +628,10 @@ describe( 'Input feature', () => {
 
 
 			editor.commands.get( 'input' ).isEnabled = false;
 			editor.commands.get( 'input' ).isEnabled = false;
 
 
-			view.fire( 'keydown', { keyCode: getCode( 'b' ) } );
+			viewDocument.fire( 'keydown', { keyCode: getCode( 'b' ) } );
 
 
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 			expect( getModelData( model ) ).to.equal( '<paragraph>fo[ob]ar</paragraph>' );
 		} );
 		} );
 	} );
 	} );
-
-	// NOTE: In all these tests we need to simulate the mutations. However, it's really tricky to tell what
-	// should be in "newChildren" because we don't have yet access to these nodes. We pass new instances,
-	// but this means that DomConverter which is used somewhere internally may return a different instance
-	// (which wouldn't happen in practice because it'd cache it). Besides, it's really hard to tell if the
-	// browser will keep the instances of the old elements when modifying the tree when the user is typing
-	// or if it will create new instances itself too.
-	// However, the code handling these mutations doesn't really care what's inside new/old children. It
-	// just needs the mutations common ancestor to understand how big fragment of the tree has changed.
-	describe( '#100', () => {
-		let domElement, domRoot;
-
-		beforeEach( () => {
-			domElement = document.createElement( 'div' );
-			document.body.appendChild( domElement );
-
-			return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, LinkEngine ] } )
-				.then( newEditor => {
-					editor = newEditor;
-					model = editor.document;
-					modelRoot = model.getRoot();
-					view = editor.editing.view;
-					viewRoot = view.getRoot();
-					domRoot = view.getDomRoot();
-
-					// Mock image feature.
-					newEditor.document.schema.registerItem( 'image', '$inline' );
-
-					buildModelConverter().for( newEditor.data.modelToView, newEditor.editing.modelToView )
-						.fromElement( 'image' )
-						.toElement( 'img' );
-
-					buildViewConverter().for( newEditor.data.viewToModel )
-						.fromElement( 'img' )
-						.toElement( 'image' );
-
-					// Disable MO completely and in a way it won't be reenabled on some Document#render() call.
-					const mutationObserver = view.getObserver( MutationObserver );
-
-					mutationObserver.disable();
-					mutationObserver.enable = () => {};
-				} );
-		} );
-
-		afterEach( () => {
-			domElement.remove();
-
-			return editor.destroy();
-		} );
-
-		// This happens when browser automatically switches parent and child nodes.
-		it( 'should handle mutations switching inner and outer node when adding new text node after', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text italic="true" linkHref="foo">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>text{}</i></a></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const link = paragraph.getChild( 0 );
-			const italic = link.getChild( 0 );
-			const text = italic.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<i><a href="foo">text</a>x</i>';
-			view.fire( 'mutations', [
-				// First mutation - remove all children from link element.
-				{
-					type: 'children',
-					node: link,
-					oldChildren: [ italic ],
-					newChildren: []
-				},
-
-				// Second mutation - remove link from paragraph and put italic there.
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ link ],
-					newChildren: [ new ViewElement( 'i' ) ]
-				},
-
-				// Third mutation - italic's new children.
-				{
-					type: 'children',
-					node: italic,
-					oldChildren: [ text ],
-					newChildren: [ new ViewElement( 'a', null, text.clone() ), new ViewText( 'x' ) ]
-				}
-			] );
-
-			expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>textx{}</i></a></p>' );
-		} );
-
-		it( 'should handle mutations switching inner and outer node when adding new text node before', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text italic="true" linkHref="foo">' +
-						'[]text' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>{}text</i></a></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const link = paragraph.getChild( 0 );
-			const italic = link.getChild( 0 );
-			const text = italic.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<i>x<a href="foo">text</a></i>';
-			view.fire( 'mutations', [
-				// First mutation - remove all children from link element.
-				{
-					type: 'children',
-					node: link,
-					oldChildren: [ italic ],
-					newChildren: []
-				},
-
-				// Second mutation - remove link from paragraph and put italic there.
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ link ],
-					newChildren: [ new ViewElement( 'i' ) ]
-				},
-
-				// Third mutation - italic's new children.
-				{
-					type: 'children',
-					node: italic,
-					oldChildren: [ text ],
-					newChildren: [ new ViewText( 'x' ), new ViewElement( 'a', null, 'text' ) ]
-				}
-			] );
-
-			expect( getViewData( view ) ).to.equal( '<p><a href="foo"><i>x{}text</i></a></p>' );
-		} );
-
-		it( 'should handle mutations switching inner and outer node - with text before', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'xxx<$text italic="true" linkHref="foo">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p>xxx<a href="foo"><i>text{}</i></a></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const textBefore = paragraph.getChild( 0 );
-			const link = paragraph.getChild( 1 );
-			const italic = link.getChild( 0 );
-			const text = italic.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = 'xxx<i><a href="foo">text</a>x</i>';
-			view.fire( 'mutations', [
-				// First mutation - remove all children from link element.
-				{
-					type: 'children',
-					node: link,
-					oldChildren: [ italic ],
-					newChildren: []
-				},
-
-				// Second mutation - remove link from paragraph and put italic there.
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ textBefore, link ],
-					newChildren: [ new ViewText( 'xxx' ), new ViewElement( 'i' ) ]
-				},
-
-				// Third mutation - italic's new children.
-				{
-					type: 'children',
-					node: italic,
-					oldChildren: [ text ],
-					newChildren: [ new ViewElement( 'a', null, 'text' ), new ViewText( 'x' ) ]
-				}
-			] );
-
-			expect( getViewData( view ) ).to.equal( '<p>xxx<a href="foo"><i>textx{}</i></a></p>' );
-		} );
-
-		// This happens when spell checker is applied on <strong> element and changes it to <b>.
-		it( 'should handle mutations replacing node', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text bold="true">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const strong = paragraph.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<b>fixed text</b>';
-			view.fire( 'mutations', [
-				// Replace `<strong>` with `<b>`.
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ strong ],
-					newChildren: [ new ViewElement( 'b', null, 'fixed text' ) ]
-				}
-			] );
-
-			expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>fixed text</strong></p>' );
-		} );
-
-		// Spell checker splits text inside attributes to two text nodes.
-		it( 'should handle mutations inside attribute element', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text bold="true">' +
-						'this is foo text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>this is foo text{}</strong></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const strong = paragraph.getChild( 0 );
-			const text = strong.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].childNodes[ 0 ].innerHTML = 'this is bar text';
-			view.fire( 'mutations', [
-				{
-					type: 'children',
-					node: strong,
-					oldChildren: [ text ],
-					newChildren: [ new ViewText( 'this is bar' ), new ViewText( ' text' ) ]
-				}
-			] );
-
-			expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>this is bar text</strong></p>' );
-		} );
-
-		it( 'should do nothing if elements mutated', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text bold="true">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const strong = paragraph.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong><img />';
-			view.fire( 'mutations', [
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ strong ],
-					newChildren: [
-						new ViewElement( 'strong', null, new ViewText( 'text' ) ),
-						new ViewElement( 'img' )
-					]
-				}
-			] );
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-		} );
-
-		it( 'should do nothing if text is not changed', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text bold="true">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const strong = paragraph.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
-			view.fire( 'mutations', [
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ strong ],
-					newChildren: [ new ViewElement( 'strong', null, new ViewText( 'text' ) ) ]
-				}
-			] );
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-		} );
-
-		it( 'should do nothing on empty mutations', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text bold="true">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
-			view.fire( 'mutations', [] );
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-		} );
-
-		it( 'should do nothing if mutations does not have common ancestor', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text bold="true">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const strong = paragraph.getChild( 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
-			view.fire( 'mutations', [
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ strong ],
-					newChildren: [ strong ]
-				},
-				{
-					type: 'children',
-					node: new ViewContainerElement( 'div' ),
-					oldChildren: [],
-					newChildren: [ new ViewText( 'foo' ), new ViewText( 'bar' ) ]
-				}
-			] );
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-		} );
-
-		it( 'should handle view selection if one is returned from mutations', () => {
-			setModelData( model,
-				'<paragraph>' +
-					'<$text bold="true">' +
-						'text[]' +
-					'</$text>' +
-				'</paragraph>'
-			);
-
-			expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const strong = paragraph.getChild( 0 );
-			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( paragraph, 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<b>textx</b>';
-			view.fire( 'mutations', [
-				// Replace `<strong>` with `<b>`.
-				{
-					type: 'children',
-					node: paragraph,
-					oldChildren: [ strong ],
-					newChildren: [ new ViewElement( 'b', null, new ViewText( 'textx' ) ) ]
-				}
-			], viewSelection );
-
-			expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]textx</$text></paragraph>' );
-			expect( getViewData( view ) ).to.equal( '<p><strong>{}textx</strong></p>' );
-		} );
-
-		// #117.
-		it( 'should handle mixed mutations', () => {
-			setModelData( model, '<paragraph><$text bold="true">Foo bar aple</$text></paragraph>' );
-
-			const paragraph = viewRoot.getChild( 0 );
-			const strong = paragraph.getChild( 0 );
-			const viewSelection = new ViewSelection();
-			viewSelection.setCollapsedAt( paragraph, 0 );
-
-			// Simulate mutations and DOM change.
-			domRoot.childNodes[ 0 ].innerHTML = '<strong>Foo bar </strong><b>apple</b>';
-			view.fire( 'mutations', [
-				{
-					type: 'text',
-					oldText: 'Foo bar aple',
-					newText: 'Foo bar ',
-					node: viewRoot.getChild( 0 ).getChild( 0 )
-				},
-				{
-					type: 'children',
-					oldChildren: [ strong ],
-					newChildren: [ strong, new ViewElement( 'b', null, new ViewText( 'apple' ) ) ],
-					node: paragraph
-				}
-			], viewSelection );
-
-			expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]Foo bar apple</$text></paragraph>' );
-			expect( getViewData( view ) ).to.equal( '<p><strong>{}Foo bar apple</strong></p>' );
-		} );
-	} );
 } );
 } );
 
 

+ 15 - 14
packages/ckeditor5-typing/tests/inputcommand-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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -21,7 +21,7 @@ import { setData as setModelData, getData as getModelData } from '@ckeditor/cked
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 
 describe( 'Typing – InputCommand integration', () => {
 describe( 'Typing – InputCommand integration', () => {
-	let editor, doc, viewDocument, boldView, italicView, editorElement;
+	let editor, model, doc, viewDocument, boldView, italicView, editorElement;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		editorElement = document.createElement( 'div' );
 		editorElement = document.createElement( 'div' );
@@ -34,7 +34,8 @@ describe( 'Typing – InputCommand integration', () => {
 			} )
 			} )
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
-				doc = editor.document;
+				model = editor.model;
+				doc = model.document;
 				viewDocument = editor.editing.view;
 				viewDocument = editor.editing.view;
 
 
 				boldView = editor.ui.componentFactory.create( 'bold' );
 				boldView = editor.ui.componentFactory.create( 'bold' );
@@ -49,7 +50,7 @@ describe( 'Typing – InputCommand integration', () => {
 	} );
 	} );
 
 
 	function expectOutput( modelOutput, viewOutput ) {
 	function expectOutput( modelOutput, viewOutput ) {
-		expect( getModelData( editor.document ) ).to.equal( modelOutput );
+		expect( getModelData( model ) ).to.equal( modelOutput );
 		expect( getViewData( viewDocument ) ).to.equal( viewOutput );
 		expect( getViewData( viewDocument ) ).to.equal( viewOutput );
 	}
 	}
 
 
@@ -72,12 +73,14 @@ describe( 'Typing – InputCommand integration', () => {
 	}
 	}
 
 
 	function setSelection( pathA, pathB ) {
 	function setSelection( pathA, pathB ) {
-		doc.selection.setRanges( [ new Range( new Position( doc.getRoot(), pathA ), new Position( doc.getRoot(), pathB ) ) ] );
+		model.change( writer => {
+			writer.setSelection( new Range( new Position( doc.getRoot(), pathA ), new Position( doc.getRoot(), pathB ) ) );
+		} );
 	}
 	}
 
 
 	describe( 'InputCommand integration', () => {
 	describe( 'InputCommand integration', () => {
 		it( 'resets the buffer on typing respecting typing.undoStep', () => {
 		it( 'resets the buffer on typing respecting typing.undoStep', () => {
-			setModelData( doc, '<paragraph>0[]</paragraph>' );
+			setModelData( model, '<paragraph>0[]</paragraph>' );
 
 
 			simulateTyping( '123456789' );
 			simulateTyping( '123456789' );
 
 
@@ -97,7 +100,7 @@ describe( 'Typing – InputCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'resets the buffer on text insertion respecting typing.undoStep', () => {
 		it( 'resets the buffer on text insertion respecting typing.undoStep', () => {
-			setModelData( doc, '<paragraph>0[]</paragraph>' );
+			setModelData( model, '<paragraph>0[]</paragraph>' );
 
 
 			simulateBatches( [ '1234', '5', '678', '9' ] );
 			simulateBatches( [ '1234', '5', '678', '9' ] );
 
 
@@ -117,7 +120,7 @@ describe( 'Typing – InputCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'resets the buffer when selection changes', () => {
 		it( 'resets the buffer when selection changes', () => {
-			setModelData( doc, '<paragraph>Foo[] Bar</paragraph>' );
+			setModelData( model, '<paragraph>Foo[] Bar</paragraph>' );
 
 
 			setSelection( [ 0, 5 ], [ 0, 5 ] );
 			setSelection( [ 0, 5 ], [ 0, 5 ] );
 			simulateTyping( '1' );
 			simulateTyping( '1' );
@@ -141,7 +144,7 @@ describe( 'Typing – InputCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'resets the buffer when selection changes (with enter)', () => {
 		it( 'resets the buffer when selection changes (with enter)', () => {
-			setModelData( doc, '<paragraph>Foo[]Bar</paragraph>' );
+			setModelData( model, '<paragraph>Foo[]Bar</paragraph>' );
 
 
 			simulateTyping( '1' );
 			simulateTyping( '1' );
 			editor.execute( 'enter' );
 			editor.execute( 'enter' );
@@ -177,7 +180,7 @@ describe( 'Typing – InputCommand integration', () => {
 		} );
 		} );
 
 
 		it( 'resets the buffer when attribute changes', () => {
 		it( 'resets the buffer when attribute changes', () => {
-			setModelData( doc, '<paragraph>Foo[] Bar</paragraph>' );
+			setModelData( model, '<paragraph>Foo[] Bar</paragraph>' );
 
 
 			simulateTyping( ' ' );
 			simulateTyping( ' ' );
 
 
@@ -192,16 +195,14 @@ describe( 'Typing – InputCommand integration', () => {
 			simulateTyping( 'z' );
 			simulateTyping( 'z' );
 
 
 			expectOutput(
 			expectOutput(
-				'<paragraph>Foo <$text bold="true">B<$text italic="true">a</$text></$text>z[] Bar</paragraph>',
+				'<paragraph>Foo <$text bold="true">B</$text><$text bold="true" italic="true">a</$text>z[] Bar</paragraph>',
 				'<p>Foo <strong>B</strong><i><strong>a</strong></i>z{} Bar</p>'
 				'<p>Foo <strong>B</strong><i><strong>a</strong></i>z{} Bar</p>'
 			);
 			);
 
 
 			editor.execute( 'undo' );
 			editor.execute( 'undo' );
 
 
 			expectOutput(
 			expectOutput(
-				'<paragraph>' +
-					'Foo <$text bold="true">B<$text italic="true">a[]</$text></$text> Bar' +
-				'</paragraph>',
+				'<paragraph>Foo <$text bold="true">B</$text><$text bold="true" italic="true">a[]</$text> Bar</paragraph>',
 				'<p>Foo <strong>B</strong><i><strong>a{}</strong></i> Bar</p>'
 				'<p>Foo <strong>B</strong><i><strong>a{}</strong></i> Bar</p>'
 			);
 			);
 
 

+ 41 - 40
packages/ckeditor5-typing/tests/inputcommand.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -14,7 +14,7 @@ import ChangeBuffer from '../src/changebuffer';
 import Input from '../src/input';
 import Input from '../src/input';
 
 
 describe( 'InputCommand', () => {
 describe( 'InputCommand', () => {
-	let editor, doc, buffer;
+	let editor, model, doc, buffer;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
@@ -22,7 +22,8 @@ describe( 'InputCommand', () => {
 		return ModelTestEditor.create()
 		return ModelTestEditor.create()
 			.then( newEditor => {
 			.then( newEditor => {
 				editor = newEditor;
 				editor = newEditor;
-				doc = editor.document;
+				model = editor.model;
+				doc = model.document;
 
 
 				const inputCommand = new InputCommand( editor, 20 );
 				const inputCommand = new InputCommand( editor, 20 );
 				editor.commands.add( 'input', inputCommand );
 				editor.commands.add( 'input', inputCommand );
@@ -30,8 +31,8 @@ describe( 'InputCommand', () => {
 				buffer = inputCommand.buffer;
 				buffer = inputCommand.buffer;
 				buffer.size = 0;
 				buffer.size = 0;
 
 
-				doc.schema.registerItem( 'p', '$block' );
-				doc.schema.registerItem( 'h1', '$block' );
+				model.schema.register( 'p', { inheritAllFrom: '$block' } );
+				model.schema.register( 'h1', { inheritAllFrom: '$block' } );
 			} );
 			} );
 	} );
 	} );
 
 
@@ -63,23 +64,23 @@ describe( 'InputCommand', () => {
 	} );
 	} );
 
 
 	describe( 'execute()', () => {
 	describe( 'execute()', () => {
-		it( 'uses enqueueChanges', () => {
-			setData( doc, '<p>foo[]bar</p>' );
+		it( 'uses enqueueChange', () => {
+			setData( model, '<p>foo[]bar</p>' );
 
 
-			doc.enqueueChanges( () => {
+			model.enqueueChange( () => {
 				editor.execute( 'input', { text: 'x' } );
 				editor.execute( 'input', { text: 'x' } );
 
 
 				// We expect that command is executed in enqueue changes block. Since we are already in
 				// We expect that command is executed in enqueue changes block. Since we are already in
 				// an enqueued block, the command execution will be postponed. Hence, no changes.
 				// an enqueued block, the command execution will be postponed. Hence, no changes.
-				expect( getData( doc ) ).to.be.equal( '<p>foo[]bar</p>' );
+				expect( getData( model ) ).to.be.equal( '<p>foo[]bar</p>' );
 			} );
 			} );
 
 
 			// After all enqueued changes are done, the command execution is reflected.
 			// After all enqueued changes are done, the command execution is reflected.
-			expect( getData( doc ) ).to.be.equal( '<p>foox[]bar</p>' );
+			expect( getData( model ) ).to.be.equal( '<p>foox[]bar</p>' );
 		} );
 		} );
 
 
 		it( 'should lock and unlock buffer', () => {
 		it( 'should lock and unlock buffer', () => {
-			setData( doc, '<p>foo[]bar</p>' );
+			setData( model, '<p>foo[]bar</p>' );
 
 
 			const spyLock = testUtils.sinon.spy( buffer, 'lock' );
 			const spyLock = testUtils.sinon.spy( buffer, 'lock' );
 			const spyUnlock = testUtils.sinon.spy( buffer, 'unlock' );
 			const spyUnlock = testUtils.sinon.spy( buffer, 'unlock' );
@@ -93,132 +94,132 @@ describe( 'InputCommand', () => {
 		} );
 		} );
 
 
 		it( 'inserts text for collapsed range', () => {
 		it( 'inserts text for collapsed range', () => {
-			setData( doc, '<p>foo[]</p>' );
+			setData( model, '<p>foo[]</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'bar',
 				text: 'bar',
-				range: editor.document.selection.getFirstRange()
+				range: doc.selection.getFirstRange()
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>foobar[]</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>foobar[]</p>' );
 			expect( buffer.size ).to.be.equal( 3 );
 			expect( buffer.size ).to.be.equal( 3 );
 		} );
 		} );
 
 
 		it( 'replaces text for range within single element on the beginning', () => {
 		it( 'replaces text for range within single element on the beginning', () => {
-			setData( doc, '<p>[fooba]r</p>' );
+			setData( model, '<p>[fooba]r</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'rab',
 				text: 'rab',
-				range: editor.document.selection.getFirstRange()
+				range: doc.selection.getFirstRange()
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>rab[]r</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>rab[]r</p>' );
 			expect( buffer.size ).to.be.equal( 3 );
 			expect( buffer.size ).to.be.equal( 3 );
 		} );
 		} );
 
 
 		it( 'replaces text for range within single element in the middle', () => {
 		it( 'replaces text for range within single element in the middle', () => {
-			setData( doc, '<p>fo[oba]r</p>' );
+			setData( model, '<p>fo[oba]r</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'bazz',
 				text: 'bazz',
-				range: editor.document.selection.getFirstRange()
+				range: doc.selection.getFirstRange()
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>fobazz[]r</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>fobazz[]r</p>' );
 			expect( buffer.size ).to.be.equal( 4 );
 			expect( buffer.size ).to.be.equal( 4 );
 		} );
 		} );
 
 
 		it( 'replaces text for range within single element on the end', () => {
 		it( 'replaces text for range within single element on the end', () => {
-			setData( doc, '<p>fooba[r]</p>' );
+			setData( model, '<p>fooba[r]</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'zzz',
 				text: 'zzz',
-				range: editor.document.selection.getFirstRange()
+				range: doc.selection.getFirstRange()
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>foobazzz[]</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>foobazzz[]</p>' );
 			expect( buffer.size ).to.be.equal( 3 );
 			expect( buffer.size ).to.be.equal( 3 );
 		} );
 		} );
 
 
 		it( 'replaces text for range within multiple elements', () => {
 		it( 'replaces text for range within multiple elements', () => {
-			setData( doc, '<h1>F[OO</h1><p>b]ar</p>' );
+			setData( model, '<h1>F[OO</h1><p>b]ar</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'unny c',
 				text: 'unny c',
-				range: editor.document.selection.getFirstRange()
+				range: doc.selection.getFirstRange()
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<h1>Funny c[</h1><p>]ar</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<h1>Funny c[</h1><p>]ar</p>' );
 			expect( buffer.size ).to.be.equal( 6 );
 			expect( buffer.size ).to.be.equal( 6 );
 		} );
 		} );
 
 
 		it( 'uses current selection when range is not given', () => {
 		it( 'uses current selection when range is not given', () => {
-			setData( doc, '<p>foob[ar]</p>' );
+			setData( model, '<p>foob[ar]</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'az'
 				text: 'az'
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>foobaz[]</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>foobaz[]</p>' );
 			expect( buffer.size ).to.be.equal( 2 );
 			expect( buffer.size ).to.be.equal( 2 );
 		} );
 		} );
 
 
 		it( 'only removes content when empty text given', () => {
 		it( 'only removes content when empty text given', () => {
-			setData( doc, '<p>[fo]obar</p>' );
+			setData( model, '<p>[fo]obar</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: '',
 				text: '',
-				range: editor.document.selection.getFirstRange()
+				range: doc.selection.getFirstRange()
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
 			expect( buffer.size ).to.be.equal( 0 );
 			expect( buffer.size ).to.be.equal( 0 );
 		} );
 		} );
 
 
 		it( 'should set selection according to passed resultRange (collapsed)', () => {
 		it( 'should set selection according to passed resultRange (collapsed)', () => {
-			setData( doc, '<p>[foo]bar</p>' );
+			setData( model, '<p>[foo]bar</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'new',
 				text: 'new',
 				resultRange: new Range( new Position( doc.getRoot(), [ 0, 5 ] ) )
 				resultRange: new Range( new Position( doc.getRoot(), [ 0, 5 ] ) )
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>newba[]r</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>newba[]r</p>' );
 			expect( buffer.size ).to.be.equal( 3 );
 			expect( buffer.size ).to.be.equal( 3 );
 		} );
 		} );
 
 
 		it( 'should set selection according to passed resultRange (non-collapsed)', () => {
 		it( 'should set selection according to passed resultRange (non-collapsed)', () => {
-			setData( doc, '<p>[foo]bar</p>' );
+			setData( model, '<p>[foo]bar</p>' );
 
 
 			editor.execute( 'input', {
 			editor.execute( 'input', {
 				text: 'new',
 				text: 'new',
 				resultRange: new Range( new Position( doc.getRoot(), [ 0, 3 ] ), new Position( doc.getRoot(), [ 0, 6 ] ) )
 				resultRange: new Range( new Position( doc.getRoot(), [ 0, 3 ] ), new Position( doc.getRoot(), [ 0, 6 ] ) )
 			} );
 			} );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>new[bar]</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>new[bar]</p>' );
 			expect( buffer.size ).to.be.equal( 3 );
 			expect( buffer.size ).to.be.equal( 3 );
 		} );
 		} );
 
 
 		it( 'only removes content when no text given (with default non-collapsed range)', () => {
 		it( 'only removes content when no text given (with default non-collapsed range)', () => {
-			setData( doc, '<p>[fo]obar</p>' );
+			setData( model, '<p>[fo]obar</p>' );
 
 
 			editor.execute( 'input' );
 			editor.execute( 'input' );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
 			expect( buffer.size ).to.be.equal( 0 );
 			expect( buffer.size ).to.be.equal( 0 );
 		} );
 		} );
 
 
 		it( 'does not change selection and content when no text given (with default collapsed range)', () => {
 		it( 'does not change selection and content when no text given (with default collapsed range)', () => {
-			setData( doc, '<p>fo[]obar</p>' );
+			setData( model, '<p>fo[]obar</p>' );
 
 
 			editor.execute( 'input' );
 			editor.execute( 'input' );
 
 
-			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>fo[]obar</p>' );
+			expect( getData( model, { selection: true } ) ).to.be.equal( '<p>fo[]obar</p>' );
 			expect( buffer.size ).to.be.equal( 0 );
 			expect( buffer.size ).to.be.equal( 0 );
 		} );
 		} );
 
 
 		it( 'does not create insert delta when no text given', () => {
 		it( 'does not create insert delta when no text given', () => {
-			setData( doc, '<p>foo[]bar</p>' );
+			setData( model, '<p>foo[]bar</p>' );
 
 
 			const version = doc.version;
 			const version = doc.version;
 
 

+ 2 - 2
packages/ckeditor5-typing/tests/manual/20/1.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -15,7 +15,7 @@ import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
-		toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 2 - 2
packages/ckeditor5-typing/tests/manual/21/1.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -15,7 +15,7 @@ import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
-		toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 2 - 2
packages/ckeditor5-typing/tests/manual/40/1.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -13,7 +13,7 @@ import Heading from '@ckeditor/ckeditor5-heading/src/heading';
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Enter, Typing, Heading ],
 		plugins: [ Enter, Typing, Heading ],
-		toolbar: [ 'headings' ]
+		toolbar: [ 'heading' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 3 - 3
packages/ckeditor5-typing/tests/manual/52/1.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -14,13 +14,13 @@ import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 window.setInterval( function() {
 window.setInterval( function() {
-	console.log( getData( window.editor.document ) );
+	console.log( getData( window.editor.model ) );
 }, 3000 );
 }, 3000 );
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
-		toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 1 - 1
packages/ckeditor5-typing/tests/manual/59/1.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 

+ 3 - 3
packages/ckeditor5-typing/tests/manual/82/1.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -12,13 +12,13 @@ import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 window.setInterval( function() {
 window.setInterval( function() {
-	console.log( getData( window.editor.document ) );
+	console.log( getData( window.editor.model ) );
 }, 3000 );
 }, 3000 );
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Essentials, Paragraph, Heading ],
 		plugins: [ Essentials, Paragraph, Heading ],
-		toolbar: [ 'headings', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 2 - 2
packages/ckeditor5-typing/tests/manual/86/1.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -11,7 +11,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 window.setInterval( function() {
 window.setInterval( function() {
-	console.log( getData( window.editor.document ) );
+	console.log( getData( window.editor.model ) );
 }, 3000 );
 }, 3000 );
 
 
 ClassicEditor
 ClassicEditor

+ 3 - 3
packages/ckeditor5-typing/tests/manual/delete.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -16,13 +16,13 @@ import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 window.getData = getData;
 window.getData = getData;
 
 
 window.setInterval( function() {
 window.setInterval( function() {
-	console.log( getData( window.editor.document ) );
+	console.log( getData( window.editor.model ) );
 }, 3000 );
 }, 3000 );
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
-		toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 3 - 3
packages/ckeditor5-typing/tests/manual/input.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -14,13 +14,13 @@ import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 window.setInterval( function() {
 window.setInterval( function() {
-	console.log( getData( window.editor.document ) );
+	console.log( getData( window.editor.model ) );
 }, 3000 );
 }, 3000 );
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
 		plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
-		toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+		toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 7 - 7
packages/ckeditor5-typing/tests/manual/nbsp.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -20,21 +20,21 @@ ClassicEditor
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;
 
 
-		editor.document.schema.allow( { name: '$text', inside: '$root' } );
+		editor.model.schema.extend( '$text', { allowIn: '$root' } );
 
 
 		const editable = editor.ui.view.editableElement;
 		const editable = editor.ui.view.editableElement;
 
 
 		document.querySelector( '#nbsp' ).addEventListener( 'click', () => {
 		document.querySelector( '#nbsp' ).addEventListener( 'click', () => {
-			editor.document.enqueueChanges( () => {
-				editor.document.selection.collapseToStart();
-				editor.document.batch().weakInsert( editor.document.selection.getFirstPosition(), '\u00A0' );
+			editor.model.change( writer => {
+				writer.setSelection( editor.model.document.selection.getFirstRange().start );
+				writer.insertText( '\u00A0', editor.model.document.selection.getFirstPosition() );
 			} );
 			} );
 		} );
 		} );
 
 
-		editor.document.on( 'changesDone', () => {
+		editor.model.document.on( 'change', () => {
 			console.clear();
 			console.clear();
 
 
-			const modelData = getModelData( editor.document, { withoutSelection: true } );
+			const modelData = getModelData( editor.model, { withoutSelection: true } );
 			console.log( 'model:', modelData.replace( /\u00A0/g, '&nbsp;' ) );
 			console.log( 'model:', modelData.replace( /\u00A0/g, '&nbsp;' ) );
 
 
 			const viewData = getViewData( editor.editing.view, { withoutSelection: true } );
 			const viewData = getViewData( editor.editing.view, { withoutSelection: true } );

+ 8 - 8
packages/ckeditor5-typing/tests/manual/rtl.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -15,15 +15,15 @@ import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 const config = {
 const config = {
 	plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
 	plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
-	toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+	toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 };
 };
 
 
 window.setInterval( function() {
 window.setInterval( function() {
-	const doc1 = window.editor1.document;
-	const doc2 = window.editor2.document;
+	const doc1 = window.editor1.model.document;
+	const doc2 = window.editor2.model.document;
 
 
-	if ( window.editor1.editing.view.isFocused ) {
-		console.log( 'editor 1', getData( doc1 ) );
+	if ( window.editor1.editing.view.document.isFocused ) {
+		console.log( 'editor 1', getData( window.editor1.model ) );
 
 
 		const modelSel = doc1.selection;
 		const modelSel = doc1.selection;
 
 
@@ -43,8 +43,8 @@ window.setInterval( function() {
 		);
 		);
 	}
 	}
 
 
-	if ( window.editor2.editing.view.isFocused ) {
-		console.log( 'editor 2', getData( doc2 ) );
+	if ( window.editor2.editing.view.document.isFocused ) {
+		console.log( 'editor 2', getData( window.editor2.model ) );
 
 
 		const modelSel = doc2.selection;
 		const modelSel = doc2.selection;
 
 

+ 1 - 1
packages/ckeditor5-typing/tests/manual/selection.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 

+ 3 - 3
packages/ckeditor5-typing/tests/manual/spellchecking.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -14,12 +14,12 @@ import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
 window.setInterval( function() {
 window.setInterval( function() {
-	console.log( getData( window.editor.document ) );
+	console.log( getData( window.editor.model ) );
 }, 3000 );
 }, 3000 );
 
 
 ClassicEditor.create( document.querySelector( '#editor' ), {
 ClassicEditor.create( document.querySelector( '#editor' ), {
 	plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
 	plugins: [ Essentials, Paragraph, Bold, Italic, Heading ],
-	toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
+	toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ]
 } )
 } )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;

+ 1 - 1
packages/ckeditor5-typing/tests/manual/unicode.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 

+ 16 - 14
packages/ckeditor5-typing/tests/spellchecking.js

@@ -1,9 +1,9 @@
 /*
 /*
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* globals window, document */
+/* globals window, document, setTimeout */
 
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
 import Enter from '@ckeditor/ckeditor5-enter/src/enter';
@@ -43,7 +43,7 @@ describe( 'Typing – spellchecking integration', () => {
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		if ( onChangesDone ) {
 		if ( onChangesDone ) {
-			editor.document.off( 'changesDone', onChangesDone );
+			editor.model.document.off( 'change', onChangesDone );
 			onChangesDone = null;
 			onChangesDone = null;
 		}
 		}
 
 
@@ -233,12 +233,12 @@ describe( 'Typing – spellchecking integration', () => {
 
 
 function emulateSpellcheckerMutation( editor, nodeIndex, resultPositionIndex, oldText, newText ) {
 function emulateSpellcheckerMutation( editor, nodeIndex, resultPositionIndex, oldText, newText ) {
 	const view = editor.editing.view;
 	const view = editor.editing.view;
-	const viewRoot = view.getRoot();
+	const viewRoot = view.document.getRoot();
 	const viewSelection = new ViewSelection();
 	const viewSelection = new ViewSelection();
 
 
-	viewSelection.setCollapsedAt( viewRoot.getChild( nodeIndex ).getChild( 0 ), resultPositionIndex );
+	viewSelection._setTo( viewRoot.getChild( nodeIndex ).getChild( 0 ), resultPositionIndex );
 
 
-	view.fire( 'mutations',
+	view.document.fire( 'mutations',
 		[ {
 		[ {
 			type: 'text',
 			type: 'text',
 			oldText,
 			oldText,
@@ -250,24 +250,26 @@ function emulateSpellcheckerMutation( editor, nodeIndex, resultPositionIndex, ol
 }
 }
 
 
 function emulateSpellcheckerInsertText( editor, nodeIndex, rangeStart, rangeEnd, text, onChangesDoneCallback ) {
 function emulateSpellcheckerInsertText( editor, nodeIndex, rangeStart, rangeEnd, text, onChangesDoneCallback ) {
-	const model = editor.editing.model;
-	const modelRoot = model.getRoot();
+	const model = editor.model;
+	const modelRoot = model.document.getRoot();
 
 
+	window.focus();
 	editor.editing.view.focus();
 	editor.editing.view.focus();
 
 
-	model.enqueueChanges( () => {
-		model.selection.setRanges( [
+	model.change( writer => {
+		writer.setSelection(
 			ModelRange.createFromParentsAndOffsets( modelRoot.getChild( nodeIndex ), rangeStart, modelRoot.getChild( nodeIndex ), rangeEnd )
 			ModelRange.createFromParentsAndOffsets( modelRoot.getChild( nodeIndex ), rangeStart, modelRoot.getChild( nodeIndex ), rangeEnd )
-		] );
+		);
 	} );
 	} );
 
 
-	editor.document.once( 'changesDone', onChangesDoneCallback, { priority: 'low' } );
+	model.document.once( 'change', () => {
+		setTimeout( onChangesDoneCallback, 100 );
+	}, { priority: 'low' } );
 
 
 	window.document.execCommand( 'insertText', false, text );
 	window.document.execCommand( 'insertText', false, text );
 }
 }
 
 
 function expectContent( editor, expectedModel, expectedView ) {
 function expectContent( editor, expectedModel, expectedView ) {
-	expect( getModelData( editor.editing.model ) ).to.equal( expectedModel );
+	expect( getModelData( editor.model ) ).to.equal( expectedModel );
 	expect( getViewData( editor.editing.view ) ).to.equal( expectedView );
 	expect( getViewData( editor.editing.view ) ).to.equal( expectedView );
 }
 }
-

+ 451 - 0
packages/ckeditor5-typing/tests/tickets/100.js

@@ -0,0 +1,451 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting';
+import Input from '../../src/input';
+
+import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
+import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
+
+import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
+import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
+import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
+import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection';
+import MutationObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mutationobserver';
+
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+
+/* global document */
+
+// NOTE: In all these tests we need to simulate the mutations. However, it's really tricky to tell what
+// should be in "newChildren" because we don't have yet access to these nodes. We pass new instances,
+// but this means that DomConverter which is used somewhere internally may return a different instance
+// (which wouldn't happen in practice because it'd cache it). Besides, it's really hard to tell if the
+// browser will keep the instances of the old elements when modifying the tree when the user is typing
+// or if it will create new instances itself too.
+// However, the code handling these mutations doesn't really care what's inside new/old children. It
+// just needs the mutations common ancestor to understand how big fragment of the tree has changed.
+describe( 'Bug ckeditor5-typing#100', () => {
+	let domElement, domRoot, editor, model, view, viewDocument, viewRoot;
+
+	beforeEach( () => {
+		domElement = document.createElement( 'div' );
+		document.body.appendChild( domElement );
+
+		return ClassicTestEditor.create( domElement, { plugins: [ Input, Paragraph, Bold, Italic, LinkEditing ] } )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				view = editor.editing.view;
+				viewDocument = view.document;
+				viewRoot = viewDocument.getRoot();
+				domRoot = view.getDomRoot();
+
+				// Mock image feature.
+				newEditor.model.schema.register( 'image', { allowWhere: '$text' } );
+
+				editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
+					model: 'image',
+					view: 'img'
+				} ) );
+
+				editor.conversion.for( 'upcast' ).add( upcastElementToElement( {
+					view: 'img',
+					model: 'image'
+				} ) );
+
+				// Disable MO completely and in a way it won't be reenabled on some Document#render() call.
+				const mutationObserver = view.getObserver( MutationObserver );
+
+				mutationObserver.disable();
+				mutationObserver.enable = () => {};
+			} );
+	} );
+
+	afterEach( () => {
+		domElement.remove();
+
+		return editor.destroy();
+	} );
+
+	// This happens when browser automatically switches parent and child nodes.
+	it( 'should handle mutations switching inner and outer node when adding new text node after', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text italic="true" bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><i><strong>text{}</strong></i></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const italic = paragraph.getChild( 0 );
+		const strong = italic.getChild( 0 );
+		const text = strong.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<strong><i>text</i>x</strong>';
+		viewDocument.fire( 'mutations', [
+			// First mutation - remove all children from link element.
+			{
+				type: 'children',
+				node: italic,
+				oldChildren: [ strong ],
+				newChildren: []
+			},
+
+			// Second mutation - remove link from paragraph and put italic there.
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ italic ],
+				newChildren: [ new ViewElement( 'i' ) ]
+			},
+
+			// Third mutation - italic's new children.
+			{
+				type: 'children',
+				node: strong,
+				oldChildren: [ text ],
+				newChildren: [ new ViewElement( 'a', null, text._clone() ), new ViewText( 'x' ) ]
+			}
+		] );
+
+		expect( getViewData( view ) ).to.equal( '<p><i><strong>textx{}</strong></i></p>' );
+	} );
+
+	it( 'should handle mutations switching inner and outer node when adding new text node before', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text italic="true" bold="true">' +
+			'[]text' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><i><strong>{}text</strong></i></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const italic = paragraph.getChild( 0 );
+		const strong = italic.getChild( 0 );
+		const text = strong.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<strong>x<i>text</i></strong>';
+		viewDocument.fire( 'mutations', [
+			// First mutation - remove all children from link element.
+			{
+				type: 'children',
+				node: italic,
+				oldChildren: [ strong ],
+				newChildren: []
+			},
+
+			// Second mutation - remove link from paragraph and put italic there.
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ italic ],
+				newChildren: [ new ViewElement( 'i' ) ]
+			},
+
+			// Third mutation - italic's new children.
+			{
+				type: 'children',
+				node: strong,
+				oldChildren: [ text ],
+				newChildren: [ new ViewText( 'x' ), new ViewElement( 'a', null, 'text' ) ]
+			}
+		] );
+
+		expect( getViewData( view ) ).to.equal( '<p><i><strong>x{}text</strong></i></p>' );
+	} );
+
+	it( 'should handle mutations switching inner and outer node - with text before', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'xxx<$text italic="true" bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p>xxx<i><strong>text{}</strong></i></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const textBefore = paragraph.getChild( 0 );
+		const link = paragraph.getChild( 1 );
+		const italic = link.getChild( 0 );
+		const text = italic.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = 'xxx<strong><i>text</i>x</strong>';
+		viewDocument.fire( 'mutations', [
+			// First mutation - remove all children from link element.
+			{
+				type: 'children',
+				node: link,
+				oldChildren: [ italic ],
+				newChildren: []
+			},
+
+			// Second mutation - remove link from paragraph and put italic there.
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ textBefore, link ],
+				newChildren: [ new ViewText( 'xxx' ), new ViewElement( 'i' ) ]
+			},
+
+			// Third mutation - italic's new children.
+			{
+				type: 'children',
+				node: italic,
+				oldChildren: [ text ],
+				newChildren: [ new ViewElement( 'a', null, 'text' ), new ViewText( 'x' ) ]
+			}
+		] );
+
+		expect( getViewData( view ) ).to.equal( '<p>xxx<i><strong>textx{}</strong></i></p>' );
+	} );
+
+	// This happens when spell checker is applied on <strong> element and changes it to <b>.
+	it( 'should handle mutations replacing node', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const strong = paragraph.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<b>fixed text</b>';
+		viewDocument.fire( 'mutations', [
+			// Replace `<strong>` with `<b>`.
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ strong ],
+				newChildren: [ new ViewElement( 'b', null, 'fixed text' ) ]
+			}
+		] );
+
+		expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>fixed text</strong></p>' );
+	} );
+
+	// Spell checker splits text inside attributes to two text nodes.
+	it( 'should handle mutations inside attribute element', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text bold="true">' +
+			'this is foo text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>this is foo text{}</strong></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const strong = paragraph.getChild( 0 );
+		const text = strong.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].childNodes[ 0 ].innerHTML = 'this is bar text';
+		viewDocument.fire( 'mutations', [
+			{
+				type: 'children',
+				node: strong,
+				oldChildren: [ text ],
+				newChildren: [ new ViewText( 'this is bar' ), new ViewText( ' text' ) ]
+			}
+		] );
+
+		expect( getViewData( view, { withoutSelection: true } ) ).to.equal( '<p><strong>this is bar text</strong></p>' );
+	} );
+
+	it( 'should do nothing if elements mutated', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const strong = paragraph.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong><img />';
+		viewDocument.fire( 'mutations', [
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ strong ],
+				newChildren: [
+					new ViewElement( 'strong', null, new ViewText( 'text' ) ),
+					new ViewElement( 'img' )
+				]
+			}
+		] );
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+	} );
+
+	it( 'should do nothing if text is not changed', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const strong = paragraph.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
+		viewDocument.fire( 'mutations', [
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ strong ],
+				newChildren: [ new ViewElement( 'strong', null, new ViewText( 'text' ) ) ]
+			}
+		] );
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+	} );
+
+	it( 'should do nothing on empty mutations', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
+		viewDocument.fire( 'mutations', [] );
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+	} );
+
+	it( 'should do nothing if mutations does not have common ancestor', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const strong = paragraph.getChild( 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<strong>text</strong>';
+		viewDocument.fire( 'mutations', [
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ strong ],
+				newChildren: [ strong ]
+			},
+			{
+				type: 'children',
+				node: new ViewContainerElement( 'div' ),
+				oldChildren: [],
+				newChildren: [ new ViewText( 'foo' ), new ViewText( 'bar' ) ]
+			}
+		] );
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+	} );
+
+	it( 'should handle view selection if one is returned from mutations', () => {
+		setModelData( model,
+			'<paragraph>' +
+			'<$text bold="true">' +
+			'text[]' +
+			'</$text>' +
+			'</paragraph>'
+		);
+
+		expect( getViewData( view ) ).to.equal( '<p><strong>text{}</strong></p>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const strong = paragraph.getChild( 0 );
+		const viewSelection = new ViewSelection( paragraph, 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<b>textx</b>';
+		viewDocument.fire( 'mutations', [
+			// Replace `<strong>` with `<b>`.
+			{
+				type: 'children',
+				node: paragraph,
+				oldChildren: [ strong ],
+				newChildren: [ new ViewElement( 'b', null, new ViewText( 'textx' ) ) ]
+			}
+		], viewSelection );
+
+		expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]textx</$text></paragraph>' );
+		expect( getViewData( view ) ).to.equal( '<p><strong>{}textx</strong></p>' );
+	} );
+
+	// #117.
+	it( 'should handle mixed mutations', () => {
+		setModelData( model, '<paragraph>[]<$text bold="true">Foo bar aple</$text></paragraph>' );
+
+		const paragraph = viewRoot.getChild( 0 );
+		const strong = paragraph.getChild( 0 );
+		const viewSelection = new ViewSelection( paragraph, 0 );
+
+		// Simulate mutations and DOM change.
+		domRoot.childNodes[ 0 ].innerHTML = '<strong>Foo bar </strong><b>apple</b>';
+		viewDocument.fire( 'mutations', [
+			{
+				type: 'text',
+				oldText: 'Foo bar aple',
+				newText: 'Foo bar ',
+				node: viewRoot.getChild( 0 ).getChild( 0 )
+			},
+			{
+				type: 'children',
+				oldChildren: [ strong ],
+				newChildren: [ strong, new ViewElement( 'b', null, new ViewText( 'apple' ) ) ],
+				node: paragraph
+			}
+		], viewSelection );
+
+		expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">[]Foo bar apple</$text></paragraph>' );
+		expect( getViewData( view ) ).to.equal( '<p><strong>{}Foo bar apple</strong></p>' );
+	} );
+} );

+ 7 - 7
packages/ckeditor5-typing/tests/tickets/59.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.
  * For licensing, see LICENSE.md.
  */
  */
 
 
@@ -35,12 +35,12 @@ describe( 'Bug ckeditor5-typing#59', () => {
 	} );
 	} );
 
 
 	it( 'editor does not blow up when deleting last styled character', () => {
 	it( 'editor does not blow up when deleting last styled character', () => {
-		editor.document.enqueueChanges( () => {
+		editor.model.change( () => {
 			editor.editing.view.getDomRoot().focus();
 			editor.editing.view.getDomRoot().focus();
-			setData( editor.document, '<paragraph><$text bold="true">foo</$text> x <$text bold="true">bar</$text>.[]</paragraph>' );
+			setData( editor.model, '<paragraph><$text bold="true">foo</$text> x <$text bold="true">bar</$text>.[]</paragraph>' );
 		} );
 		} );
 
 
-		while ( editor.document.selection.anchor.offset > 0 ) {
+		while ( editor.model.document.selection.anchor.offset > 0 ) {
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 		}
 		}
 
 
@@ -50,12 +50,12 @@ describe( 'Bug ckeditor5-typing#59', () => {
 	// This is something that came to my mind after I worked on ckeditor/ckeditor5-engine#659.
 	// This is something that came to my mind after I worked on ckeditor/ckeditor5-engine#659.
 	// Toggling bold at the end creates a lot of weird cases so it's interesting to see if it works... and it didn't back then.
 	// Toggling bold at the end creates a lot of weird cases so it's interesting to see if it works... and it didn't back then.
 	it( 'editor does not blow up when deleting last styled character, forcing bold switch', () => {
 	it( 'editor does not blow up when deleting last styled character, forcing bold switch', () => {
-		editor.document.enqueueChanges( () => {
+		editor.model.change( () => {
 			editor.editing.view.getDomRoot().focus();
 			editor.editing.view.getDomRoot().focus();
-			setData( editor.document, '<paragraph><$text bold="true">foo</$text> x <$text bold="true">bar</$text>.[]</paragraph>' );
+			setData( editor.model, '<paragraph><$text bold="true">foo</$text> x <$text bold="true">bar</$text>.[]</paragraph>' );
 		} );
 		} );
 
 
-		while ( editor.document.selection.anchor.offset > 0 ) {
+		while ( editor.model.document.selection.anchor.offset > 0 ) {
 			editor.execute( 'delete' );
 			editor.execute( 'delete' );
 			editor.execute( 'bold' );
 			editor.execute( 'bold' );
 		}
 		}

+ 1 - 1
packages/ckeditor5-typing/tests/typing.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.
  * For licensing, see LICENSE.md.
  */
  */