Przeglądaj źródła

Merge branch 'master' into t/1281

Kamil Piechaczek 7 lat temu
rodzic
commit
a1ec48f520
100 zmienionych plików z 4487 dodań i 2967 usunięć
  1. 13 5
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 21 18
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  3. 80 81
      packages/ckeditor5-engine/src/conversion/downcast-converters.js
  4. 10 8
      packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js
  5. 40 26
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  6. 38 48
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  7. 3 2
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  8. 15 10
      packages/ckeditor5-engine/src/dev-utils/model.js
  9. 22 17
      packages/ckeditor5-engine/src/dev-utils/view.js
  10. 74 2
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  11. 14 3
      packages/ckeditor5-engine/src/view/attributeelement.js
  12. 3 3
      packages/ckeditor5-engine/src/view/containerelement.js
  13. 2 242
      packages/ckeditor5-engine/src/view/document.js
  14. 1 1
      packages/ckeditor5-engine/src/view/domconverter.js
  15. 19 8
      packages/ckeditor5-engine/src/view/editableelement.js
  16. 166 153
      packages/ckeditor5-engine/src/view/element.js
  17. 3 3
      packages/ckeditor5-engine/src/view/filler.js
  18. 5 5
      packages/ckeditor5-engine/src/view/observer/clickobserver.js
  19. 13 5
      packages/ckeditor5-engine/src/view/observer/domeventdata.js
  20. 3 3
      packages/ckeditor5-engine/src/view/observer/domeventobserver.js
  21. 6 6
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  22. 8 9
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  23. 5 7
      packages/ckeditor5-engine/src/view/observer/keyobserver.js
  24. 4 5
      packages/ckeditor5-engine/src/view/observer/mouseobserver.js
  25. 12 13
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  26. 14 6
      packages/ckeditor5-engine/src/view/observer/observer.js
  27. 10 20
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  28. 35 29
      packages/ckeditor5-engine/src/view/placeholder.js
  29. 1 1
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  30. 43 40
      packages/ckeditor5-engine/src/view/selection.js
  31. 3 3
      packages/ckeditor5-engine/src/view/uielement.js
  32. 415 0
      packages/ckeditor5-engine/src/view/view.js
  33. 0 8
      packages/ckeditor5-engine/src/view/view.jsdoc
  34. 1087 557
      packages/ckeditor5-engine/src/view/writer.js
  35. 10 10
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  36. 27 66
      packages/ckeditor5-engine/tests/conversion/downcast-converters.js
  37. 73 60
      packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js
  38. 34 20
      packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js
  39. 8 9
      packages/ckeditor5-engine/tests/conversion/two-way-converters.js
  40. 19 20
      packages/ckeditor5-engine/tests/conversion/upcast-converters.js
  41. 6 5
      packages/ckeditor5-engine/tests/conversion/upcast-selection-converters.js
  42. 28 26
      packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js
  43. 4 4
      packages/ckeditor5-engine/tests/conversion/viewconsumable.js
  44. 2 1
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  45. 28 22
      packages/ckeditor5-engine/tests/dev-utils/view.js
  46. 16 14
      packages/ckeditor5-engine/tests/manual/highlight.js
  47. 2 2
      packages/ckeditor5-engine/tests/manual/markers.js
  48. 5 7
      packages/ckeditor5-engine/tests/manual/nestededitable.js
  49. 5 4
      packages/ckeditor5-engine/tests/manual/placeholder.js
  50. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/880/1.js
  51. 9 8
      packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js
  52. 394 0
      packages/ckeditor5-engine/tests/model/utils/modifyselection.js
  53. 1 1
      packages/ckeditor5-engine/tests/view/_utils/createroot.js
  54. 4 4
      packages/ckeditor5-engine/tests/view/attributeelement.js
  55. 61 0
      packages/ckeditor5-engine/tests/view/document.js
  56. 0 414
      packages/ckeditor5-engine/tests/view/document/document.js
  57. 0 83
      packages/ckeditor5-engine/tests/view/document/integration.js
  58. 1 1
      packages/ckeditor5-engine/tests/view/domconverter/binding.js
  59. 1 2
      packages/ckeditor5-engine/tests/view/domconverter/domconverter.js
  60. 3 9
      packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js
  61. 15 38
      packages/ckeditor5-engine/tests/view/editableelement.js
  62. 98 98
      packages/ckeditor5-engine/tests/view/element.js
  63. 17 16
      packages/ckeditor5-engine/tests/view/manual/clickobserver.js
  64. 19 20
      packages/ckeditor5-engine/tests/view/manual/fakeselection.js
  65. 29 15
      packages/ckeditor5-engine/tests/view/manual/focus.js
  66. 21 10
      packages/ckeditor5-engine/tests/view/manual/focusobserver.js
  67. 16 6
      packages/ckeditor5-engine/tests/view/manual/immutable.js
  68. 16 11
      packages/ckeditor5-engine/tests/view/manual/inline-filler.js
  69. 15 6
      packages/ckeditor5-engine/tests/view/manual/keyobserver.js
  70. 10 8
      packages/ckeditor5-engine/tests/view/manual/mutationobserver.js
  71. 7 6
      packages/ckeditor5-engine/tests/view/manual/noselection-iframe.js
  72. 13 6
      packages/ckeditor5-engine/tests/view/manual/noselection.js
  73. 9 8
      packages/ckeditor5-engine/tests/view/manual/selectionobserver.js
  74. 15 20
      packages/ckeditor5-engine/tests/view/manual/uielement.js
  75. 11 9
      packages/ckeditor5-engine/tests/view/manual/x-index.js
  76. 5 5
      packages/ckeditor5-engine/tests/view/matcher.js
  77. 9 10
      packages/ckeditor5-engine/tests/view/node.js
  78. 6 5
      packages/ckeditor5-engine/tests/view/observer/clickobserver.js
  79. 9 7
      packages/ckeditor5-engine/tests/view/observer/domeventdata.js
  80. 25 24
      packages/ckeditor5-engine/tests/view/observer/domeventobserver.js
  81. 11 10
      packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js
  82. 28 23
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  83. 6 5
      packages/ckeditor5-engine/tests/view/observer/keyobserver.js
  84. 6 5
      packages/ckeditor5-engine/tests/view/observer/mouseobserver.js
  85. 36 34
      packages/ckeditor5-engine/tests/view/observer/mutationobserver.js
  86. 4 3
      packages/ckeditor5-engine/tests/view/observer/observer.js
  87. 37 34
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  88. 63 49
      packages/ckeditor5-engine/tests/view/placeholder.js
  89. 1 3
      packages/ckeditor5-engine/tests/view/position.js
  90. 125 108
      packages/ckeditor5-engine/tests/view/renderer.js
  91. 4 4
      packages/ckeditor5-engine/tests/view/rooteditableelement.js
  92. 99 101
      packages/ckeditor5-engine/tests/view/selection.js
  93. 2 2
      packages/ckeditor5-engine/tests/view/textproxy.js
  94. 0 4
      packages/ckeditor5-engine/tests/view/treewalker.js
  95. 39 38
      packages/ckeditor5-engine/tests/view/view/jumpoverinlinefiller.js
  96. 73 33
      packages/ckeditor5-engine/tests/view/view/jumpoveruielement.js
  97. 596 0
      packages/ckeditor5-engine/tests/view/view/view.js
  98. 18 11
      packages/ckeditor5-engine/tests/view/writer/breakattributes.js
  99. 22 17
      packages/ckeditor5-engine/tests/view/writer/breakcontainer.js
  100. 22 15
      packages/ckeditor5-engine/tests/view/writer/clear.js

+ 13 - 5
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -19,6 +19,8 @@ import UpcastDispatcher from '../conversion/upcastdispatcher';
 import { convertText, convertToModelFragment } from '../conversion/upcast-converters';
 
 import ViewDocumentFragment from '../view/documentfragment';
+import ViewDocument from '../view/document';
+import ViewWriter from '../view/writer';
 
 import ModelRange from '../model/range';
 
@@ -74,7 +76,7 @@ export default class DataController {
 		 * @readonly
 		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher}
 		 */
-		this.downcastDispatcher = new DowncastDispatcher( this.model, {
+		this.downcastDispatcher = new DowncastDispatcher( {
 			mapper: this.mapper
 		} );
 		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
@@ -85,7 +87,7 @@ export default class DataController {
 		 * @readonly
 		 * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher}
 		 */
-		this.upcastDispatcher = new UpcastDispatcher( this.model, {
+		this.upcastDispatcher = new UpcastDispatcher( {
 			schema: model.schema
 		} );
 
@@ -143,9 +145,13 @@ export default class DataController {
 		const modelRange = ModelRange.createIn( modelElementOrFragment );
 
 		const viewDocumentFragment = new ViewDocumentFragment();
+
+		// Create separate ViewWriter just for data conversion purposes.
+		// We have no view controller and rendering do DOM in DataController so view.change() block is not used here.
+		const viewWriter = new ViewWriter( new ViewDocument() );
 		this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
 
-		this.downcastDispatcher.convertInsert( modelRange );
+		this.downcastDispatcher.convertInsert( modelRange, viewWriter );
 
 		if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
 			// Then, if a document element is converted, convert markers.
@@ -153,7 +159,7 @@ export default class DataController {
 			const markers = _getMarkersRelativeToElement( modelElementOrFragment );
 
 			for ( const [ name, range ] of markers ) {
-				this.downcastDispatcher.convertMarkerAdd( name, range );
+				this.downcastDispatcher.convertMarkerAdd( name, range, viewWriter );
 			}
 		}
 
@@ -221,7 +227,9 @@ export default class DataController {
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
 	 */
 	toModel( viewElementOrFragment, context = '$root' ) {
-		return this.upcastDispatcher.convert( viewElementOrFragment, context );
+		return this.model.change( writer => {
+			return this.upcastDispatcher.convert( viewElementOrFragment, writer, context );
+		} );
 	}
 
 	/**

+ 21 - 18
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -8,7 +8,7 @@
  */
 
 import RootEditableElement from '../view/rooteditableelement';
-import ViewDocument from '../view/document';
+import View from '../view/view';
 import Mapper from '../conversion/mapper';
 import DowncastDispatcher from '../conversion/downcastdispatcher';
 import {
@@ -49,12 +49,12 @@ export default class EditingController {
 		this.model = model;
 
 		/**
-		 * View document.
+		 * Editing view controller.
 		 *
 		 * @readonly
-		 * @member {module:engine/view/document~Document}
+		 * @member {module:engine/view/view~View}
 		 */
-		this.view = new ViewDocument();
+		this.view = new View();
 
 		/**
 		 * Mapper which describes the model-view binding.
@@ -70,24 +70,23 @@ export default class EditingController {
 		 * @readonly
 		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
 		 */
-		this.downcastDispatcher = new DowncastDispatcher( this.model, {
-			mapper: this.mapper,
-			viewSelection: this.view.selection
+		this.downcastDispatcher = new DowncastDispatcher( {
+			mapper: this.mapper
 		} );
 
 		const doc = this.model.document;
+		const selection = doc.selection;
+		const markers = this.model.markers;
 
 		this.listenTo( doc, 'change', () => {
-			this.downcastDispatcher.convertChanges( doc.differ );
-		}, { priority: 'low' } );
-
-		this.listenTo( model, '_change', () => {
-			this.downcastDispatcher.convertSelection( doc.selection );
-			this.view.render();
+			this.view.change( writer => {
+				this.downcastDispatcher.convertChanges( doc.differ, writer );
+				this.downcastDispatcher.convertSelection( selection, markers, writer );
+			} );
 		}, { priority: 'low' } );
 
 		// Convert selection from view to model.
-		this.listenTo( this.view, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
+		this.listenTo( this.view.document, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
 
 		// Attach default model converters.
 		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
@@ -122,7 +121,9 @@ export default class EditingController {
 				if ( _operationAffectsMarker( operation, marker ) ) {
 					// And if the operation in any way modifies the marker, remove the marker from the view.
 					removedMarkers.add( marker.name );
-					this.downcastDispatcher.convertMarkerRemove( marker.name, markerRange );
+					this.view.change( writer => {
+						this.downcastDispatcher.convertMarkerRemove( marker.name, markerRange, writer );
+					} );
 
 					// TODO: This stinks but this is the safest place to have this code.
 					this.model.document.differ.bufferMarkerChange( marker.name, markerRange, markerRange );
@@ -135,7 +136,9 @@ export default class EditingController {
 		this.listenTo( model.markers, 'remove', ( evt, marker ) => {
 			if ( !removedMarkers.has( marker.name ) ) {
 				removedMarkers.add( marker.name );
-				this.downcastDispatcher.convertMarkerRemove( marker.name, marker.getRange() );
+				this.view.change( writer => {
+					this.downcastDispatcher.convertMarkerRemove( marker.name, marker.getRange(), writer );
+				} );
 			}
 		} );
 
@@ -147,7 +150,7 @@ export default class EditingController {
 		// Binds {@link module:engine/view/document~Document#roots view roots collection} to
 		// {@link module:engine/model/document~Document#roots model roots collection} so creating
 		// model root automatically creates corresponding view root.
-		this.view.roots.bindTo( this.model.document.roots ).using( root => {
+		this.view.document.roots.bindTo( this.model.document.roots ).using( root => {
 			// $graveyard is a special root that has no reflection in the view.
 			if ( root.rootName == '$graveyard' ) {
 				return null;
@@ -156,7 +159,7 @@ export default class EditingController {
 			const viewRoot = new RootEditableElement( root.name );
 
 			viewRoot.rootName = root.rootName;
-			viewRoot.document = this.view;
+			viewRoot._document = this.view.document;
 			this.mapper.bindElements( root, viewRoot );
 
 			return viewRoot;

+ 80 - 81
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -11,9 +11,7 @@ import ViewContainerElement from '../view/containerelement';
 import ViewUIElement from '../view/uielement';
 import ViewElement from '../view/element';
 import ViewAttributeElement from '../view/attributeelement';
-import ViewText from '../view/text';
 import ViewRange from '../view/range';
-import viewWriter from '../view/writer';
 import DocumentSelection from '../model/documentselection';
 
 import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
@@ -33,8 +31,6 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *		downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
  *
- *		downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } );
- *
  *		downcastElementToElement( {
  *			model: 'fancyParagraph',
  *			view: {
@@ -45,17 +41,23 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  * 		downcastElementToElement( {
  * 			model: 'heading',
- * 			view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+ * 			view: ( modelItem, consumable, conversionApi ) => {
+ * 				const viewWriter = conversionApi.writer;
+ *
+ * 				return viewWriter.createContainerElement( 'h' + modelItem.getAttribute( 'level' ) );
+ * 			}
  * 		} );
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  * @param {Object} config Conversion configuration.
  * @param {String} config.model Name of the model element to convert.
- * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function|
- * module:engine/view/containerelement~ContainerElement} config.view View element name, or a view element definition,
- * or a function that takes model element as a parameter and returns a view container element,
- * or a view container element instance. The view element will be used then in conversion.
+ * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name, or a
+ * view element definition, or a function that  will be provided with all the parameters of the dispatcher's
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
+ * It's expected that the function returns a {@link module:engine/view/containerelement~ContainerElement}.
+ * The view element will be used then in conversion.
+ *
  * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  */
@@ -79,8 +81,6 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  *
  *		downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' );
  *
- *		downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } );
- *
  *		downcastAttributeToElement( 'bold', {
  *			view: {
  *				name: 'span',
@@ -118,7 +118,11 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  *		] );
  *
  * 		downcastAttributeToElement( 'bold', {
- * 			view: modelAttributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } )
+ * 			view: ( modelAttributeValue, data, consumable, conversionApi ) => {
+ * 				const viewWriter = conversionApi.writer;
+ *
+ * 				return viewWriter( 'span', { style: 'font-weight:' + modelAttributeValue } );
+ * 			}
  * 		} );
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
@@ -127,9 +131,8 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  * @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
  * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
  * If omitted, the configuration item will be used as a "default" configuration when no other item matches the attribute value.
- * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function|
- * module:engine/view/attributeelement~AttributeElement} config.view View element name, or a view element definition,
- * or a function that takes model element as a parameter and returns a view attribute element, or a view attribute element instance.
+ * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name,
+ * or a view element definition, or a function that takes model element as a parameter and returns a view attribute element.
  * The view element will be used then in conversion.
  * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
@@ -248,14 +251,14 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  * 		downcastMarkerToElement( {
  * 			model: 'search',
- * 			view: data => {
- *	 			return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+ * 			view: ( data, conversionApi ) => {
+ *	 			return conversionApi.writer.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
  * 			}
  * 		} );
  *
  * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
  * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
- * The `data` object properties are passed from
+ * The `data` and `conversionApi` objects are passed from
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
  * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
  * marker end boundary element.
@@ -319,8 +322,8 @@ export function downcastMarkerToElement( config, priority = 'normal' ) {
  * 		} );
  *
  * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
- * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
- * The `data` object properties are passed from
+ * receives `data` and `conversionApi` objects as parameters and should return
+ * {@link module:engine/conversion/downcast-converters~HighlightDescriptor}. The `data` and `conversionApi` objects are passed from
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
@@ -355,17 +358,19 @@ function _normalizeToElementConfig( config, ViewElementClass ) {
 		return;
 	}
 
+	const view = config.view;
+
 	// Build `.view` property.
-	// It is expected to be either creator function or view element instance.
-	if ( typeof config.view == 'string' ) {
-		// If `.view` is a string, create a proper view element instance out of given `ViewElementClass` and name given in `.view`.
-		config.view = new ViewElementClass( config.view );
-	} else if ( typeof config.view == 'object' && !( config.view instanceof ViewElementClass ) ) {
+	// It is expected to be either string, element definition or creator function.
+	if ( typeof view == 'string' ) {
+		// If `.view` is a string, create a function that returns view element instance out of given `ViewElementClass`.
+		config.view = () => new ViewElementClass( view );
+	} else if ( typeof view == 'object' ) {
 		// If `.view` is an object, use it to build view element instance.
-		config.view = _createViewElementFromDefinition( config.view, ViewElementClass );
+		const element = _createViewElementFromDefinition( view, ViewElementClass );
+		config.view = () => element.clone();
 	}
-	// `.view` can be also a function or already a view element instance.
-	// These are already valid types which don't have to be normalized.
+	// `.view` can be also a function that is already valid type which don't have to be normalized.
 }
 
 // Creates view element instance from provided viewElementDefinition and class.
@@ -377,17 +382,11 @@ function _createViewElementFromDefinition( viewElementDefinition, ViewElementCla
 	const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
 
 	if ( viewElementDefinition.style ) {
-		element.setStyle( viewElementDefinition.style );
+		element._setStyle( viewElementDefinition.style );
 	}
 
 	if ( viewElementDefinition.class ) {
-		const classes = viewElementDefinition.class;
-
-		if ( typeof classes == 'string' ) {
-			element.addClass( classes );
-		} else {
-			element.addClass( ...classes );
-		}
+		element._addClass( viewElementDefinition.class );
 	}
 
 	return element;
@@ -445,7 +444,7 @@ function _getCreatorForArrayConfig( config ) {
 	const defaultConfig = config.find( configEntry => configEntry.model === undefined );
 
 	// Return a creator function.
-	return modelAttributeValue => {
+	return ( modelAttributeValue, data, consumable, conversionApi ) => {
 		// Set default config at first. It will be used if no other entry matches model attribute value.
 		let matchedConfigEntry = defaultConfig;
 
@@ -460,12 +459,8 @@ function _getCreatorForArrayConfig( config ) {
 
 		// If there was default config or matched config...
 		if ( matchedConfigEntry ) {
-			// If the entry `.view` is a function, execute it and return the value...
-			if ( typeof matchedConfigEntry.view == 'function' ) {
-				return matchedConfigEntry.view( modelAttributeValue );
-			}
-			// Else, just return `.view`, it should be a view element instance after it got normalized earlier.
-			return matchedConfigEntry.view;
+			// The entry `.view` is a function after it got normalized earlier, execute it and return the value.
+			return matchedConfigEntry.view( modelAttributeValue, data, consumable, conversionApi );
 		}
 
 		return null;
@@ -474,21 +469,20 @@ function _getCreatorForArrayConfig( config ) {
 
 /**
  * Function factory, creates a converter that converts node insertion changes from the model to the view.
- * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
- * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
- * dispatcher's {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
+ * Passed function will be provided with all the parameters of the dispatcher's
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * The result of the function will be inserted to the view.
  *
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and bind model and view elements.
  *
- *		modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
- *
- *		modelDispatcher.on(
+ *		downcastDispatcher.on(
  *			'insert:myElem',
  *			insertElement( ( modelItem, consumable, conversionApi ) => {
- *				let myElem = new ViewElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, new ViewText( 'myText' ) );
+ *				const writer = conversionApi.writer;
+ *				const text = writer.createText( 'myText' );
+ *				const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
  *
  *				// Do something fancy with myElem using `modelItem` or other parameters.
  *
@@ -496,15 +490,12 @@ function _getCreatorForArrayConfig( config ) {
  *			}
  *		) );
  *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which
- * will be inserted.
+ * @param {Function} elementCreator Function returning a view element, which will be inserted.
  * @returns {Function} Insert element event converter.
  */
 export function insertElement( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.item, consumable, conversionApi );
+		const viewElement = elementCreator( data.item, consumable, conversionApi );
 
 		if ( !viewElement ) {
 			return;
@@ -517,7 +508,7 @@ export function insertElement( elementCreator ) {
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 
 		conversionApi.mapper.bindElements( data.item, viewElement );
-		viewWriter.insert( viewPosition, viewElement );
+		conversionApi.writer.insert( viewPosition, viewElement );
 	};
 }
 
@@ -537,8 +528,9 @@ export function insertText() {
 			return;
 		}
 
+		const viewWriter = conversionApi.writer;
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-		const viewText = new ViewText( data.item.data );
+		const viewText = viewWriter.createText( data.item.data );
 
 		viewWriter.insert( viewPosition, viewText );
 	};
@@ -562,7 +554,7 @@ export function remove() {
 		const viewRange = new ViewRange( viewStart, viewEnd );
 
 		// Trim the range to remove in case some UI elements are on the view range boundaries.
-		const removed = viewWriter.remove( viewRange.getTrimmed() );
+		const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
 
 		// After the range is removed, unbind all view elements from the model.
 		// Range inside view document fragment is used to unbind deeply.
@@ -623,6 +615,7 @@ export function insertUIElement( elementCreator ) {
 		}
 
 		const mapper = conversionApi.mapper;
+		const viewWriter = conversionApi.writer;
 
 		// Add "opening" element.
 		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
@@ -666,6 +659,7 @@ export function removeUIElement( elementCreator ) {
 		}
 
 		const markerRange = data.markerRange;
+		const viewWriter = conversionApi.writer;
 
 		// When removing the ui elements, we map the model range to view twice, because that view range
 		// may change after the first clearing.
@@ -722,16 +716,20 @@ export function changeAttribute( attributeCreator ) {
 
 		// First remove the old attribute if there was one.
 		const oldAttribute = attributeCreator( data.attributeOldValue, data, consumable, conversionApi );
+		const mapper = conversionApi.mapper;
+		const viewWriter = conversionApi.writer;
 
 		if ( data.attributeOldValue !== null && oldAttribute ) {
-			conversionApi.mapper.toViewElement( data.item ).removeAttribute( oldAttribute.key );
+			const viewElement = mapper.toViewElement( data.item );
+			viewWriter.removeAttribute( oldAttribute.key, viewElement );
 		}
 
 		// Then, if conversion was successful, set the new attribute.
 		const newAttribute = attributeCreator( data.attributeNewValue, data, consumable, conversionApi );
 
 		if ( data.attributeNewValue !== null && newAttribute ) {
-			conversionApi.mapper.toViewElement( data.item ).setAttribute( newAttribute.key, newAttribute.value );
+			const viewElement = mapper.toViewElement( data.item );
+			viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
 		}
 	};
 }
@@ -750,8 +748,7 @@ export function changeAttribute( attributeCreator ) {
  *			|- b {bold: true}                             |   |- ab
  *			|- c                                          |- c
  *
- * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
- * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
+ * Passed `Function` will be provided with attribute value and then all the parameters of the
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute event}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * The result of the function will be the wrapping element.
@@ -760,24 +757,21 @@ export function changeAttribute( attributeCreator ) {
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  *
- *		modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+ *		modelDispatcher.on( 'attribute:bold', wrapItem( ( attributeValue, data, consumable, conversionApi ) => {
+ *			return conversionApi.writer.createAttributeElement( 'strong' );
+ *		} );
  *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
- * be used for wrapping.
+ * @param {Function} elementCreator Function returning a view element, which will be used for wrapping.
  * @returns {Function} Set/change attribute converter.
  */
 export function wrap( elementCreator ) {
 	return ( evt, data, consumable, conversionApi ) => {
 		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
 		// or the attribute was removed.
-		const oldViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeOldValue, data, consumable, conversionApi );
+		const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
 
 		// Create node to wrap with.
-		const newViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeNewValue, data, consumable, conversionApi );
+		const newViewElement = elementCreator( data.attributeNewValue, data, consumable, conversionApi );
 
 		if ( !oldViewElement && !newViewElement ) {
 			return;
@@ -787,9 +781,12 @@ export function wrap( elementCreator ) {
 			return;
 		}
 
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
 			// Selection attribute conversion.
-			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), newViewElement, conversionApi.viewSelection );
+			viewWriter.wrap( viewSelection.getFirstRange(), newViewElement );
 		} else {
 			// Node attribute conversion.
 			let viewRange = conversionApi.mapper.toViewRange( data.range );
@@ -842,9 +839,11 @@ export function highlightText( highlightDescriptor ) {
 		}
 
 		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
 
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
-			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), viewElement, conversionApi.viewSelection );
+			viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection );
 		} else {
 			const viewRange = conversionApi.mapper.toViewRange( data.range );
 			viewWriter.wrap( viewRange, viewElement );
@@ -856,7 +855,7 @@ export function highlightText( highlightDescriptor ) {
  * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range.
  *
  * The converter checks if an element has `addHighlight` function stored as
- * {@link module:engine/view/element~Element#setCustomProperty custom property} and, if so, uses it to apply the highlight.
+ * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
  * In such case converter will consume all element's children, assuming that they were handled by element itself.
  *
  * When `addHighlight` custom property is not present, element is not converted in any special way.
@@ -900,7 +899,7 @@ export function highlightElement( highlightDescriptor ) {
 				consumable.consume( value.item, evt.name );
 			}
 
-			viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor );
+			viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer );
 		}
 	};
 }
@@ -914,7 +913,7 @@ export function highlightElement( highlightDescriptor ) {
  * highlight descriptor. See {link module:engine/conversion/downcast-converters~highlightDescriptorToAttributeElement}.
  *
  * For elements, the converter checks if an element has `removeHighlight` function stored as
- * {@link module:engine/view/element~Element#setCustomProperty custom property}. If so, it uses it to remove the highlight.
+ * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
  * In such case, children of that element will not be converted.
  *
  * When `removeHighlight` is not present, element is not converted in any special way.
@@ -948,7 +947,7 @@ export function removeHighlight( highlightDescriptor ) {
 		// First, iterate through all items and remove highlight from those container elements that have custom highlight handling.
 		for ( const item of items ) {
 			if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) {
-				item.getCustomProperty( 'removeHighlight' )( item, descriptor.id );
+				item.getCustomProperty( 'removeHighlight' )( item, descriptor.id, conversionApi.writer );
 
 				// If container element had custom handling, remove all it's children from further processing.
 				for ( const descendant of ViewRange.createIn( item ) ) {
@@ -960,6 +959,7 @@ export function removeHighlight( highlightDescriptor ) {
 		// Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end
 		// to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged).
 		const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
+		const viewWriter = conversionApi.writer;
 
 		for ( const item of Array.from( items ).reverse() ) {
 			if ( item.is( 'textProxy' ) ) {
@@ -1005,15 +1005,14 @@ export function createViewElementFromHighlightDescriptor( descriptor ) {
 	const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
 
 	if ( descriptor.class ) {
-		const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ];
-		viewElement.addClass( ...cssClasses );
+		viewElement._addClass( descriptor.class );
 	}
 
 	if ( descriptor.priority ) {
-		viewElement.priority = descriptor.priority;
+		viewElement._priority = descriptor.priority;
 	}
 
-	viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id );
+	viewElement._setCustomProperty( 'highlightDescriptorId', descriptor.id );
 
 	return viewElement;
 }

+ 10 - 8
packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-import viewWriter from '../view/writer';
-
 /**
  * Contains {@link module:engine/model/selection~Selection model selection} to
  * {@link module:engine/view/selection~Selection view selection} converters for
@@ -41,7 +39,7 @@ export function convertRangeSelection() {
 			viewRanges.push( viewRange );
 		}
 
-		conversionApi.viewSelection.setTo( viewRanges, selection.isBackward );
+		conversionApi.writer.setSelection( viewRanges, selection.isBackward );
 	};
 }
 
@@ -79,11 +77,12 @@ export function convertCollapsedSelection() {
 			return;
 		}
 
+		const viewWriter = conversionApi.writer;
 		const modelPosition = selection.getFirstPosition();
 		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
 		const brokenPosition = viewWriter.breakAttributes( viewPosition );
 
-		conversionApi.viewSelection.setTo( brokenPosition );
+		viewWriter.setSelection( brokenPosition );
 	};
 }
 
@@ -113,16 +112,19 @@ export function convertCollapsedSelection() {
  */
 export function clearAttributes() {
 	return ( evt, data, consumable, conversionApi ) => {
-		for ( const range of conversionApi.viewSelection.getRanges() ) {
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
+		for ( const range of viewSelection.getRanges() ) {
 			// Not collapsed selection should not have artifacts.
 			if ( range.isCollapsed ) {
 				// Position might be in the node removed by the view writer.
 				if ( range.end.parent.document ) {
-					viewWriter.mergeAttributes( range.start );
+					conversionApi.writer.mergeAttributes( range.start );
 				}
 			}
 		}
-		conversionApi.viewSelection.setTo( null );
+		viewWriter.setSelection( null );
 	};
 }
 
@@ -131,5 +133,5 @@ export function clearAttributes() {
  * {@link module:engine/model/selection~Selection model selection} conversion.
  */
 export function clearFakeSelection() {
-	return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection.setFake( false );
+	return ( evt, data, consumable, conversionApi ) => conversionApi.writer.setFakeSelection( false );
 }

+ 40 - 26
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -73,6 +73,9 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
  * converted the change should also stop the event (for efficiency purposes).
  *
+ * When providing custom listeners for `DowncastDispatcher` remember to use provided
+ * {@link module:engine/view/writer~Writer view writer} to apply changes to the view document.
+ *
  * Example of a custom converter for `DowncastDispatcher`:
  *
  *		// We will convert inserting "paragraph" model element into the model.
@@ -86,13 +89,13 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  *			const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  *
  *			// Create <p> element that will be inserted in view at `viewPosition`.
- *			const viewElement = new ViewElement( 'p' );
+ *			const viewElement = conversionApi.writer.createContainerElement( 'p' );
  *
  *			// Bind the newly created view element to model element so positions will map accordingly in future.
  *			conversionApi.mapper.bindElements( data.item, viewElement );
  *
  *			// Add the newly created view element to the view.
- *			viewWriter.insert( viewPosition, viewElement );
+ *			conversionApi.writer.insert( viewPosition, viewElement );
  *
  *			// Remember to stop the event propagation.
  *			evt.stop();
@@ -102,18 +105,9 @@ export default class DowncastDispatcher {
 	/**
 	 * Creates a `DowncastDispatcher` instance.
 	 *
-	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls.
 	 */
-	constructor( model, conversionApi = {} ) {
-		/**
-		 * Data model instance bound with this dispatcher.
-		 *
-		 * @private
-		 * @member {module:engine/model/model~Model}
-		 */
-		this._model = model;
-
+	constructor( conversionApi = {} ) {
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
@@ -126,23 +120,24 @@ export default class DowncastDispatcher {
 	 * Takes {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
 	 *
 	 * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
 	 */
-	convertChanges( differ ) {
+	convertChanges( differ, writer ) {
 		// Convert changes that happened on model tree.
 		for ( const entry of differ.getChanges() ) {
 			if ( entry.type == 'insert' ) {
-				this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ) );
+				this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ), writer );
 			} else if ( entry.type == 'remove' ) {
-				this.convertRemove( entry.position, entry.length, entry.name );
+				this.convertRemove( entry.position, entry.length, entry.name, writer );
 			} else {
 				// entry.type == 'attribute'.
-				this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue );
+				this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer );
 			}
 		}
 
 		// After the view is updated, convert markers which has changed.
 		for ( const change of differ.getMarkersToAdd() ) {
-			this.convertMarkerAdd( change.name, change.range );
+			this.convertMarkerAdd( change.name, change.range, writer );
 		}
 	}
 
@@ -155,8 +150,11 @@ export default class DowncastDispatcher {
 	 * @fires insert
 	 * @fires attribute
 	 * @param {module:engine/model/range~Range} range Inserted range.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
 	 */
-	convertInsert( range ) {
+	convertInsert( range, writer ) {
+		this.conversionApi.writer = writer;
+
 		// Create a list of things that can be consumed, consisting of nodes and their attributes.
 		const consumable = this._createInsertConsumable( range );
 
@@ -190,8 +188,11 @@ export default class DowncastDispatcher {
 	 * @param {module:engine/model/position~Position} position Position from which node was removed.
 	 * @param {Number} length Offset size of removed node.
 	 * @param {String} name Name of removed node.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
 	 */
-	convertRemove( position, length, name ) {
+	convertRemove( position, length, name, writer ) {
+		this.conversionApi.writer = writer;
+
 		this.fire( 'remove:' + name, { position, length }, this.conversionApi );
 	}
 
@@ -205,8 +206,11 @@ export default class DowncastDispatcher {
 	 * @param {String} key Key of the attribute that has changed.
 	 * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
 	 * @param {*} newValue New attribute value or `null` if the attribute has been removed.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
 	 */
-	convertAttribute( range, key, oldValue, newValue ) {
+	convertAttribute( range, key, oldValue, newValue, writer ) {
+		this.conversionApi.writer = writer;
+
 		// Create a list with attributes to consume.
 		const consumable = this._createConsumableForRange( range, `attribute:${ key }` );
 
@@ -235,10 +239,14 @@ export default class DowncastDispatcher {
 	 * @fires addMarker
 	 * @fires attribute
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
+	 * @param {module:engine/model/selection~Selection} Array<module:engine/model/markercollection~Marker> markers
+	 * Array of markers containing model markers.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
 	 */
-	convertSelection( selection ) {
-		const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
-		const consumable = this._createSelectionConsumable( selection, markers );
+	convertSelection( selection, markers, writer ) {
+		this.conversionApi.writer = writer;
+		const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
+		const consumable = this._createSelectionConsumable( selection, markersAtSelection );
 
 		this.fire( 'selection', { selection }, consumable, this.conversionApi );
 
@@ -246,7 +254,7 @@ export default class DowncastDispatcher {
 			return;
 		}
 
-		for ( const marker of markers ) {
+		for ( const marker of markersAtSelection ) {
 			const markerRange = marker.getRange();
 
 			if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
@@ -287,13 +295,16 @@ export default class DowncastDispatcher {
 	 * @fires addMarker
 	 * @param {String} markerName Marker name.
 	 * @param {module:engine/model/range~Range} markerRange Marker range.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
 	 */
-	convertMarkerAdd( markerName, markerRange ) {
+	convertMarkerAdd( markerName, markerRange, writer ) {
 		// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
 		if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
 			return;
 		}
 
+		this.conversionApi.writer = writer;
+
 		// In markers' case, event name == consumable name.
 		const eventName = 'addMarker:' + markerName;
 
@@ -332,13 +343,16 @@ export default class DowncastDispatcher {
 	 * @fires removeMarker
 	 * @param {String} markerName Marker name.
 	 * @param {module:engine/model/range~Range} markerRange Marker range.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
 	 */
-	convertMarkerRemove( markerName, markerRange ) {
+	convertMarkerRemove( markerName, markerRange, writer ) {
 		// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
 		if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
 			return;
 		}
 
+		this.conversionApi.writer = writer;
+
 		this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
 	}
 

+ 38 - 48
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -97,19 +97,10 @@ export default class UpcastDispatcher {
 	 * Creates a `UpcastDispatcher` that operates using passed API.
 	 *
 	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi
-	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
 	 * by `UpcastDispatcher`.
 	 */
-	constructor( model, conversionApi = {} ) {
-		/**
-		 * Data model.
-		 *
-		 * @private
-		 * @type {module:engine/model/model~Model}
-		 */
-		this._model = model;
-
+	constructor( conversionApi = {} ) {
 		/**
 		 * List of elements that will be checked after conversion process and if element in the list will be empty it
 		 * will be removed from conversion result.
@@ -153,62 +144,61 @@ export default class UpcastDispatcher {
 	 * @fires documentFragment
 	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
 	 * Part of the view to be converted.
+	 * @param {module:engine/model/writer~Writer} writer Instance of model writer.
 	 * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
 	 * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
 	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
 	 */
-	convert( viewItem, context = [ '$root' ] ) {
-		return this._model.change( writer => {
-			this.fire( 'viewCleanup', viewItem );
+	convert( viewItem, writer, context = [ '$root' ] ) {
+		this.fire( 'viewCleanup', viewItem );
 
-			// Create context tree and set position in the top element.
-			// Items will be converted according to this position.
-			this._modelCursor = createContextTree( context, writer );
+		// Create context tree and set position in the top element.
+		// Items will be converted according to this position.
+		this._modelCursor = createContextTree( context, writer );
 
-			// Store writer in conversion as a conversion API
-			// to be sure that conversion process will use the same batch.
-			this.conversionApi.writer = writer;
+		// Store writer in conversion as a conversion API
+		// to be sure that conversion process will use the same batch.
+		this.conversionApi.writer = writer;
 
-			// Create consumable values list for conversion process.
-			this.conversionApi.consumable = ViewConsumable.createFrom( viewItem );
+		// Create consumable values list for conversion process.
+		this.conversionApi.consumable = ViewConsumable.createFrom( viewItem );
 
-			// Custom data stored by converter for conversion process.
-			this.conversionApi.store = {};
+		// Custom data stored by converter for conversion process.
+		this.conversionApi.store = {};
 
-			// Do the conversion.
-			const { modelRange } = this._convertItem( viewItem, this._modelCursor );
+		// Do the conversion.
+		const { modelRange } = this._convertItem( viewItem, this._modelCursor );
 
-			// Conversion result is always a document fragment so let's create this fragment.
-			const documentFragment = writer.createDocumentFragment();
+		// Conversion result is always a document fragment so let's create this fragment.
+		const documentFragment = writer.createDocumentFragment();
 
-			// When there is a conversion result.
-			if ( modelRange ) {
-				// Remove all empty elements that was added to #_removeIfEmpty list.
-				this._removeEmptyElements();
-
-				// Move all items that was converted to context tree to document fragment.
-				for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
-					writer.append( item, documentFragment );
-				}
+		// When there is a conversion result.
+		if ( modelRange ) {
+			// Remove all empty elements that was added to #_removeIfEmpty list.
+			this._removeEmptyElements();
 
-				// Extract temporary markers elements from model and set as static markers collection.
-				documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
+			// Move all items that was converted to context tree to document fragment.
+			for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
+				writer.append( item, documentFragment );
 			}
 
-			// Clear context position.
-			this._modelCursor = null;
+			// Extract temporary markers elements from model and set as static markers collection.
+			documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
+		}
+
+		// Clear context position.
+		this._modelCursor = null;
 
-			// Clear split elements.
-			this._removeIfEmpty.clear();
+		// Clear split elements.
+		this._removeIfEmpty.clear();
 
-			// Clear conversion API.
-			this.conversionApi.writer = null;
-			this.conversionApi.store = null;
+		// Clear conversion API.
+		this.conversionApi.writer = null;
+		this.conversionApi.store = null;
 
-			// Return fragment as conversion result.
-			return documentFragment;
-		} );
+		// Return fragment as conversion result.
+		return documentFragment;
 	}
 
 	/**

+ 3 - 2
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -628,7 +628,7 @@ function enableDocumentTools() {
 	} );
 
 	sandbox.mock( Editor.prototype, 'logView', function( version ) {
-		this.editing.view.log( version );
+		this.editing.view.document.log( version );
 	} );
 
 	sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) {
@@ -658,7 +658,8 @@ class DebugPlugin extends Plugin {
 
 		const model = this.editor.model;
 		const modelDocument = model.document;
-		const viewDocument = this.editor.editing.view;
+		const view = this.editor.editing.view;
+		const viewDocument = view.document;
 
 		modelDocument[ treeDump ] = [];
 		viewDocument[ treeDump ] = [];

+ 15 - 10
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -21,8 +21,8 @@ import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
 import DocumentSelection from '../model/documentselection';
 
+import View from '../view/view';
 import UpcastDispatcher from '../conversion/upcastdispatcher';
-import ViewDocument from '../view/document';
 import ViewContainerElement from '../view/containerelement';
 import ViewAttributeElement from '../view/attributeelement';
 import ViewRootEditableElement from '../view/rooteditableelement';
@@ -191,17 +191,18 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	}
 
 	// Set up conversion.
-	// Create a temporary view document.
-	const viewDocument = new ViewDocument();
+	// Create a temporary view controller.
+	const view = new View();
+	const viewDocument = view.document;
 	const viewRoot = new ViewRootEditableElement( 'div' );
 
 	// Create a temporary root element in view document.
-	viewRoot.document = viewDocument;
+	viewRoot._document = view.document;
 	viewRoot.rootName = 'main';
 	viewDocument.roots.add( viewRoot );
 
 	// Create and setup downcast dispatcher.
-	const downcastDispatcher = new DowncastDispatcher( model, { mapper, viewSelection: viewDocument.selection } );
+	const downcastDispatcher = new DowncastDispatcher( { mapper } );
 
 	// Bind root elements.
 	mapper.bindElements( node.root, viewRoot );
@@ -222,11 +223,12 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 
 	// Convert model to view.
-	downcastDispatcher.convertInsert( range );
+	const writer = view._writer;
+	downcastDispatcher.convertInsert( range, writer );
 
 	// Convert model selection to view selection.
 	if ( selection ) {
-		downcastDispatcher.convertSelection( selection );
+		downcastDispatcher.convertSelection( selection, model.markers, writer );
 	}
 
 	// Parse view to data string.
@@ -235,7 +237,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	// Removing unneccessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
 	data = data.substr( 5, data.length - 11 );
 
-	viewDocument.destroy();
+	view.destroy();
 
 	// Replace valid XML `model-text-with-attributes` element name to `$text`.
 	return data.replace( new RegExp( 'model-text-with-attributes', 'g' ), '$text' );
@@ -283,7 +285,8 @@ export function parse( data, schema, options = {} ) {
 	}
 
 	// Set up upcast dispatcher.
-	const upcastDispatcher = new UpcastDispatcher( new Model(), { schema, mapper } );
+	const modelController = new Model();
+	const upcastDispatcher = new UpcastDispatcher( { schema, mapper } );
 
 	upcastDispatcher.on( 'documentFragment', convertToModelFragment() );
 	upcastDispatcher.on( 'element:model-text-with-attributes', convertToModelText( true ) );
@@ -293,7 +296,9 @@ export function parse( data, schema, options = {} ) {
 	upcastDispatcher.isDebug = true;
 
 	// Convert view to model.
-	let model = upcastDispatcher.convert( viewDocumentFragment.root, options.context || '$root' );
+	let model = modelController.change(
+		writer => upcastDispatcher.convert( viewDocumentFragment.root, writer, options.context || '$root' )
+	);
 
 	mapper.bindElements( model, viewDocumentFragment.root );
 

+ 22 - 17
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -11,7 +11,7 @@
  * Collection of methods for manipulating the {@link module:engine/view/view view} for testing purposes.
  */
 
-import Document from '../view/document';
+import View from '../view/view';
 import ViewDocumentFragment from '../view/documentfragment';
 import XmlDataProcessor from '../dataprocessor/xmldataprocessor';
 import ViewElement from '../view/element';
@@ -37,7 +37,7 @@ const allowedTypes = {
 /**
  * Writes the content of the {@link module:engine/view/document~Document document} to an HTML-like string.
  *
- * @param {module:engine/view/document~Document} document
+ * @param {module:engine/view/view~View} view
  * @param {Object} [options]
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will
  * not be included in the returned string.
@@ -49,11 +49,12 @@ const allowedTypes = {
  * (`<span view-priority="12">`, `<b view-priority="10">`).
  * @returns {String} The stringified data.
  */
-export function getData( document, options = {} ) {
-	if ( !( document instanceof Document ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/view/document~Document.' );
+export function getData( view, options = {} ) {
+	if ( !( view instanceof View ) ) {
+		throw new TypeError( 'View needs to be an instance of module:engine/view/view~View.' );
 	}
 
+	const document = view.document;
 	const withoutSelection = !!options.withoutSelection;
 	const rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
@@ -74,24 +75,28 @@ getData._stringify = stringify;
 /**
  * Sets the content of the {@link module:engine/view/document~Document document} provided as an HTML-like string.
  *
- * @param {module:engine/view/document~Document} document
+ * @param {module:engine/view/view~View} view
  * @param {String} data An HTML-like string to write into the document.
  * @param {Object} options
  * @param {String} [options.rootName='main'] The root name where parsed data will be stored. If not provided,
  * the default `main` name will be used.
  */
-export function setData( document, data, options = {} ) {
-	if ( !( document instanceof Document ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/view/document~Document.' );
+export function setData( view, data, options = {} ) {
+	if ( !( view instanceof View ) ) {
+		throw new TypeError( 'View needs to be an instance of module:engine/view/view~View.' );
 	}
 
+	const document = view.document;
 	const rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
-	const result = setData._parse( data, { rootElement: root } );
 
-	if ( result.view && result.selection ) {
-		document.selection.setTo( result.selection );
-	}
+	view.change( writer => {
+		const result = setData._parse( data, { rootElement: root } );
+
+		if ( result.view && result.selection ) {
+			writer.setSelection( result.selection );
+		}
+	} );
 }
 
 // Set parse as setData private method - needed for testing/spying.
@@ -187,7 +192,7 @@ setData._parse = parse;
  * {@link module:engine/view/attributeelement~AttributeElement attribute elements}.
  *
  *		const attribute = new AttributeElement( 'b' );
- *		attribute.priority = 20;
+ *		attribute._priority = 20;
  *		getData( attribute, null, { showPriority: true } ); // <b view-priority="20"></b>
  *
  * @param {module:engine/view/text~Text|module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
@@ -919,13 +924,13 @@ function _convertElement( viewElement ) {
 
 	if ( newElement.is( 'attributeElement' ) ) {
 		if ( info.priority !== null ) {
-			newElement.priority = info.priority;
+			newElement._priority = info.priority;
 		}
 	}
 
 	// Move attributes.
 	for ( const attributeKey of viewElement.getAttributeKeys() ) {
-		newElement.setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
+		newElement._setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
 	}
 
 	return newElement;
@@ -947,7 +952,7 @@ function _convertElement( viewElement ) {
 function _convertElementNameAndPriority( viewElement ) {
 	const parts = viewElement.name.split( ':' );
 	const priority = _convertPriority( viewElement.getAttribute( 'view-priority' ) );
-	viewElement.removeAttribute( 'view-priority' );
+	viewElement._removeAttribute( 'view-priority' );
 
 	if ( parts.length == 1 ) {
 		return {

+ 74 - 2
packages/ckeditor5-engine/src/model/utils/modifyselection.js

@@ -13,6 +13,8 @@ import Range from '../range';
 import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
 import DocumentSelection from '../documentselection';
 
+const wordBoundaryCharacters = ' ,.?!:;"-()';
+
 /**
  * Modifies the selection. Currently, the supported modifications are:
  *
@@ -31,6 +33,7 @@ import DocumentSelection from '../documentselection';
  *  For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
  *  outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
  *  extension will include whole "surrogate pair".
+ *  * `'word'` - moves selection by a whole word.
  *
  * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
  *
@@ -39,7 +42,7 @@ import DocumentSelection from '../documentselection';
  * @param {module:engine/model/selection~Selection} selection The selection to modify.
  * @param {Object} [options]
  * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
- * @param {'character'|'codePoint'} [options.unit='character'] The unit by which selection should be modified.
+ * @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
  */
 export default function modifySelection( model, selection, options = {} ) {
 	const schema = model.schema;
@@ -79,11 +82,17 @@ export default function modifySelection( model, selection, options = {} ) {
 }
 
 // Checks whether the selection can be extended to the the walker's next value (next position).
+// @param {{ walker, unit, isForward, schema }} data
+// @param {module:engine/view/treewalker~TreeWalkerValue} value
 function tryExtendingTo( data, value ) {
 	// If found text, we can certainly put the focus in it. Let's just find a correct position
 	// based on the unit.
 	if ( value.type == 'text' ) {
-		return getCorrectPosition( data.walker, data.unit );
+		if ( data.unit === 'word' ) {
+			return getCorrectWordBreakPosition( data.walker, data.isForward );
+		}
+
+		return getCorrectPosition( data.walker, data.unit, data.isForward );
 	}
 
 	// Entering an element.
@@ -117,6 +126,9 @@ function tryExtendingTo( data, value ) {
 
 // Finds a correct position by walking in a text node and checking whether selection can be extended to given position
 // or should be extended further.
+//
+// @param {module:engine/model/treewalker~TreeWalker} walker
+// @param {String} unit The unit by which selection should be modified.
 function getCorrectPosition( walker, unit ) {
 	const textNode = walker.position.textNode;
 
@@ -134,6 +146,45 @@ function getCorrectPosition( walker, unit ) {
 	return walker.position;
 }
 
+// Finds a correct position of a word break by walking in a text node and checking whether selection can be extended to given position
+// or should be extended further.
+//
+// @param {module:engine/model/treewalker~TreeWalker} walker
+// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
+function getCorrectWordBreakPosition( walker, isForward ) {
+	let textNode = walker.position.textNode;
+
+	if ( textNode ) {
+		let offset = walker.position.offset - textNode.startOffset;
+
+		while ( !isAtWordBoundary( textNode.data, offset, isForward ) && !isAtNodeBoundary( textNode, offset, isForward ) ) {
+			walker.next();
+
+			// Check of adjacent text nodes with different attributes (like BOLD).
+			// Example          : 'foofoo []bar<$text bold="true">bar</$text> bazbaz'
+			// should expand to : 'foofoo [bar<$text bold="true">bar</$text>] bazbaz'.
+			const nextNode = isForward ? walker.position.nodeAfter : walker.position.nodeBefore;
+
+			if ( nextNode ) {
+				// Check boundary char of an adjacent text node.
+				const boundaryChar = nextNode.data.charAt( isForward ? 0 : nextNode.data.length - 1 );
+
+				// Go to the next node if the character at the boundary of that node belongs to the same word.
+				if ( !wordBoundaryCharacters.includes( boundaryChar ) ) {
+					// If adjacent text node belongs to the same word go to it & reset values.
+					walker.next();
+
+					textNode = walker.position.textNode;
+				}
+			}
+
+			offset = walker.position.offset - textNode.startOffset;
+		}
+	}
+
+	return walker.position;
+}
+
 function getSearchRange( start, isForward ) {
 	const root = start.root;
 	const searchEnd = Position.createAt( root, isForward ? 'end' : 0 );
@@ -144,3 +195,24 @@ function getSearchRange( start, isForward ) {
 		return new Range( searchEnd, start );
 	}
 }
+
+// Checks if selection is on word boundary.
+//
+// @param {String} data The text node value to investigate.
+// @param {Number} offset Position offset.
+// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
+function isAtWordBoundary( data, offset, isForward ) {
+	// The offset to check depends on direction.
+	const offsetToCheck = offset + ( isForward ? 0 : -1 );
+
+	return wordBoundaryCharacters.includes( data.charAt( offsetToCheck ) );
+}
+
+// Checks if selection is on node boundary.
+//
+// @param {module:engine/model/text~Text} textNode The text node to investigate.
+// @param {Number} offset Position offset.
+// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
+function isAtNodeBoundary( textNode, offset, isForward ) {
+	return offset === ( isForward ? textNode.endOffset : 0 );
+}

+ 14 - 3
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -14,7 +14,7 @@ const DEFAULT_PRIORITY = 10;
 
 /**
  * Attributes are elements which define document presentation. They are mostly elements like `<b>` or `<span>`.
- * Attributes can be broken and merged by the {@link module:engine/view/writer~writer view writer}.
+ * Attributes can be broken and merged by the {@link module:engine/view/writer~Writer view writer}.
  *
  * Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
  * be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement}
@@ -36,9 +36,10 @@ export default class AttributeElement extends Element {
 		 * {@link module:engine/view/element~Element#isSimilar similar}. Setting different priorities on similar
  		 * nodes may prevent merging, e.g. two `<abbr>` nodes next each other shouldn't be merged.
 		 *
+		 * @protected
 		 * @member {Number}
 		 */
-		this.priority = DEFAULT_PRIORITY;
+		this._priority = DEFAULT_PRIORITY;
 
 		/**
 		 * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
@@ -49,6 +50,16 @@ export default class AttributeElement extends Element {
 		this.getFillerOffset = getFillerOffset;
 	}
 
+	/**
+	 * Priority of this element.
+	 *
+	 * @readonly
+	 * @return {Number}
+	 */
+	get priority() {
+		return this._priority;
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -71,7 +82,7 @@ export default class AttributeElement extends Element {
 		const cloned = super.clone( deep );
 
 		// Clone priority too.
-		cloned.priority = this.priority;
+		cloned._priority = this._priority;
 
 		return cloned;
 	}

+ 3 - 3
packages/ckeditor5-engine/src/view/containerelement.js

@@ -23,8 +23,8 @@ import Element from './element';
  * DOM properly. {@link module:engine/view/domconverter~DomConverter} will ensure that `ContainerElement` is editable and it is possible
  * to put caret inside it, even if the container is empty.
  *
- * Secondly, {@link module:engine/view/writer~writer view writer} uses this information.
- * Nodes {@link module:engine/view/writer~writer.breakAttributes breaking} and {@link module:engine/view/writer~writer.mergeAttributes
+ * Secondly, {@link module:engine/view/writer~Writer view writer} uses this information.
+ * Nodes {@link module:engine/view/writer~Writer#breakAttributes breaking} and {@link module:engine/view/writer~Writer#mergeAttributes
  * merging}
  * is performed only in a bounds of a container nodes.
  *
@@ -32,7 +32,7 @@ import Element from './element';
  *
  *		<p><b>fo^o</b></p>
  *
- * {@link module:engine/view/writer~writer.breakAttributes breakAttributes} will create:
+ * {@link module:engine/view/writer~Writer#breakAttributes breakAttributes} will create:
  *
  *		<p><b>fo</b><b>o</b></p>
  *

+ 2 - 242
packages/ckeditor5-engine/src/view/document.js

@@ -8,37 +8,13 @@
  */
 
 import Selection from './selection';
-import Renderer from './renderer';
-import DomConverter from './domconverter';
-import { injectQuirksHandling } from './filler';
-import { injectUiElementHandling } from './uielement';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-import MutationObserver from './observer/mutationobserver';
-import SelectionObserver from './observer/selectionobserver';
-import FocusObserver from './observer/focusobserver';
-import KeyObserver from './observer/keyobserver';
-import FakeSelectionObserver from './observer/fakeselectionobserver';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
 
 /**
- * Document class creates an abstract layer over the content editable area.
- * It combines the actual tree of view elements, tree of DOM elements,
- * {@link module:engine/view/domconverter~DomConverter DOM Converter}, {@link module:engine/view/renderer~Renderer renderer} and all
- * {@link module:engine/view/observer/observer~Observer observers}.
- *
- * If you want to only transform the tree of view elements to the DOM elements you can use the
- * {@link module:engine/view/domconverter~DomConverter DomConverter}.
- *
- * Note that the following observers are added by the class constructor and are always available:
- *
- * * {@link module:engine/view/observer/mutationobserver~MutationObserver},
- * * {@link module:engine/view/observer/selectionobserver~SelectionObserver},
- * * {@link module:engine/view/observer/focusobserver~FocusObserver},
- * * {@link module:engine/view/observer/keyobserver~KeyObserver},
- * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
+ * Document class creates an abstract layer over the content editable area, contains a tree of view elements and
+ * {@link module:engine/view/selection~Selection view selection} associated with this document.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -47,14 +23,6 @@ export default class Document {
 	 * Creates a Document instance.
 	 */
 	constructor() {
-		/**
-		 * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
-		 *
-		 * @readonly
-		 * @member {Map} module:engine/view/document~Document#domRoots
-		 */
-		this.domRoots = new Map();
-
 		/**
 		 * Selection done on this document.
 		 *
@@ -63,16 +31,6 @@ export default class Document {
 		 */
 		this.selection = new Selection();
 
-		/**
-		 * Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by
-		 * {@link module:engine/view/document~Document#renderer renderer}
-		 * and {@link module:engine/view/observer/observer~Observer observers}.
-		 *
-		 * @readonly
-		 * @member {module:engine/view/domconverter~DomConverter} module:engine/view/document~Document#domConverter
-		 */
-		this.domConverter = new DomConverter();
-
 		/**
 		 * Roots of the view tree. Collection of the {module:engine/view/element~Element view elements}.
 		 *
@@ -107,109 +65,6 @@ export default class Document {
 		 * @member {Boolean} module:engine/view/document~Document#isFocused
 		 */
 		this.set( 'isFocused', false );
-
-		/**
-		 * Instance of the {@link module:engine/view/document~Document#renderer renderer}.
-		 *
-		 * @readonly
-		 * @member {module:engine/view/renderer~Renderer} module:engine/view/document~Document#renderer
-		 */
-		this.renderer = new Renderer( this.domConverter, this.selection );
-		this.renderer.bind( 'isFocused' ).to( this );
-
-		/**
-		 * Map of registered {@link module:engine/view/observer/observer~Observer observers}.
-		 *
-		 * @private
-		 * @member {Map.<Function, module:engine/view/observer/observer~Observer>} module:engine/view/document~Document#_observers
-		 */
-		this._observers = new Map();
-
-		// Add default observers.
-		this.addObserver( MutationObserver );
-		this.addObserver( SelectionObserver );
-		this.addObserver( FocusObserver );
-		this.addObserver( KeyObserver );
-		this.addObserver( FakeSelectionObserver );
-
-		injectQuirksHandling( this );
-		injectUiElementHandling( this );
-
-		this.decorate( 'render' );
-	}
-
-	/**
-	 * Creates observer of the given type if not yet created, {@link module:engine/view/observer/observer~Observer#enable enables} it
-	 * and {@link module:engine/view/observer/observer~Observer#observe attaches} to all existing and future
-	 * {@link module:engine/view/document~Document#domRoots DOM roots}.
-	 *
-	 * Note: Observers are recognized by their constructor (classes). A single observer will be instantiated and used only
-	 * when registered for the first time. This means that features and other components can register a single observer
-	 * multiple times without caring whether it has been already added or not.
-	 *
-	 * @param {Function} Observer The constructor of an observer to add.
-	 * Should create an instance inheriting from {@link module:engine/view/observer/observer~Observer}.
-	 * @returns {module:engine/view/observer/observer~Observer} Added observer instance.
-	 */
-	addObserver( Observer ) {
-		let observer = this._observers.get( Observer );
-
-		if ( observer ) {
-			return observer;
-		}
-
-		observer = new Observer( this );
-
-		this._observers.set( Observer, observer );
-
-		for ( const [ name, domElement ] of this.domRoots ) {
-			observer.observe( domElement, name );
-		}
-
-		observer.enable();
-
-		return observer;
-	}
-
-	/**
-	 * Returns observer of the given type or `undefined` if such observer has not been added yet.
-	 *
-	 * @param {Function} Observer The constructor of an observer to get.
-	 * @returns {module:engine/view/observer/observer~Observer|undefined} Observer instance or undefined.
-	 */
-	getObserver( Observer ) {
-		return this._observers.get( Observer );
-	}
-
-	/**
-	 * Attaches DOM root element to the view element and enable all observers on that element.
-	 * Also {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view
-	 * what means that all child nodes will be removed and replaced with content of the view root.
-	 *
-	 * This method also will change view element name as the same as tag name of given dom root.
-	 * Name is always transformed to lower case.
-	 *
-	 * @param {Element} domRoot DOM root element.
-	 * @param {String} [name='main'] Name of the root.
-	 */
-	attachDomRoot( domRoot, name = 'main' ) {
-		const viewRoot = this.getRoot( name );
-
-		// Set view root name the same as DOM root tag name.
-		viewRoot._name = domRoot.tagName.toLowerCase();
-
-		this.domRoots.set( name, domRoot );
-		this.domConverter.bindElements( domRoot, viewRoot );
-		this.renderer.markToSync( 'children', viewRoot );
-		this.renderer.domDocuments.add( domRoot.ownerDocument );
-
-		viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
-		viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
-		viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
-
-		for ( const observer of this._observers.values() ) {
-			observer.observe( domRoot, name );
-		}
 	}
 
 	/**
@@ -223,94 +78,6 @@ export default class Document {
 	getRoot( name = 'main' ) {
 		return this.roots.get( name );
 	}
-
-	/**
-	 * Gets DOM root element.
-	 *
-	 * @param {String} [name='main']  Name of the root.
-	 * @returns {Element} DOM root element instance.
-	 */
-	getDomRoot( name = 'main' ) {
-		return this.domRoots.get( name );
-	}
-
-	/**
-	 * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
-	 * before rendering and re-enabled after that.
-	 *
-	 * @fires render
-	 */
-	render() {
-		this.disableObservers();
-		this.renderer.render();
-		this.enableObservers();
-	}
-
-	/**
-	 * Focuses document. It will focus {@link module:engine/view/editableelement~EditableElement EditableElement} that is currently having
-	 * selection inside.
-	 */
-	focus() {
-		if ( !this.isFocused ) {
-			const editable = this.selection.editableElement;
-
-			if ( editable ) {
-				this.domConverter.focus( editable );
-				this.render();
-			} else {
-				/**
-				 * Before focusing view document, selection should be placed inside one of the view's editables.
-				 * Normally its selection will be converted from model document (which have default selection), but
-				 * when using view document on its own, we need to manually place selection before focusing it.
-				 *
-				 * @error view-focus-no-selection
-				 */
-				log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
-			}
-		}
-	}
-
-	/**
-	 * Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
-	 * caret, if not already visible to the user.
-	 */
-	scrollToTheSelection() {
-		const range = this.selection.getFirstRange();
-
-		if ( range ) {
-			scrollViewportToShowTarget( {
-				target: this.domConverter.viewRangeToDom( range ),
-				viewportOffset: 20
-			} );
-		}
-	}
-
-	/**
-	 * Disables all added observers.
-	 */
-	disableObservers() {
-		for ( const observer of this._observers.values() ) {
-			observer.disable();
-		}
-	}
-
-	/**
-	 * Enables all added observers.
-	 */
-	enableObservers() {
-		for ( const observer of this._observers.values() ) {
-			observer.enable();
-		}
-	}
-
-	/**
-	 * Destroys all observers created by view `Document`.
-	 */
-	destroy() {
-		for ( const observer of this._observers.values() ) {
-			observer.destroy();
-		}
-	}
 }
 
 mix( Document, ObservableMixin );
@@ -326,10 +93,3 @@ mix( Document, ObservableMixin );
  *
  * @typedef {String} module:engine/view/document~ChangeType
  */
-
-/**
- * Fired when {@link #render render} method is called. Actual rendering is executed as a listener to
- * this event with default priority. This way other listeners can be used to run code before or after rendering.
- *
- * @event render
- */

+ 1 - 1
packages/ckeditor5-engine/src/view/domconverter.js

@@ -413,7 +413,7 @@ export default class DomConverter {
 				const attrs = domNode.attributes;
 
 				for ( let i = attrs.length - 1; i >= 0; i-- ) {
-					viewElement.setAttribute( attrs[ i ].name, attrs[ i ].value );
+					viewElement._setAttribute( attrs[ i ].name, attrs[ i ].value );
 				}
 			}
 

+ 19 - 8
packages/ckeditor5-engine/src/view/editableelement.js

@@ -41,8 +41,8 @@ export default class EditableElement extends ContainerElement {
 		/**
 		 * Whether the editable is focused.
 		 *
-		 * This property updates when {@link module:engine/view/document~Document#isFocused document.isFocused} is changed and after each
-		 * {@link module:engine/view/document~Document#render render} method call.
+		 * This property updates when {@link module:engine/view/document~Document#isFocused document.isFocused} or view
+		 * selection is changed.
 		 *
 		 * @readonly
 		 * @observable
@@ -60,11 +60,23 @@ export default class EditableElement extends ContainerElement {
 		 */
 	}
 
+	/**
+	 * Returns document associated with the editable.
+	 *
+	 * @readonly
+	 * @return {module:engine/view/document~Document}
+	 */
 	get document() {
 		return this.getCustomProperty( documentSymbol );
 	}
 
-	set document( document ) {
+	/**
+	 * Sets document of this editable element.
+	 *
+	 * @protected
+	 * @param {module:engine/view/document~Document} document
+	 */
+	set _document( document ) {
 		if ( this.getCustomProperty( documentSymbol ) ) {
 			/**
 			 * View document is already set. It can only be set once.
@@ -74,7 +86,7 @@ export default class EditableElement extends ContainerElement {
 			throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.' );
 		}
 
-		this.setCustomProperty( documentSymbol, document );
+		this._setCustomProperty( documentSymbol, document );
 
 		this.bind( 'isReadOnly' ).to( document );
 
@@ -84,11 +96,10 @@ export default class EditableElement extends ContainerElement {
 			isFocused => isFocused && document.selection.editableElement == this
 		);
 
-		// Update focus state before each rendering. Rendering should not change neither the selection nor the value of
-		// document.isFocused property.
-		this.listenTo( document, 'render', () => {
+		// Update focus state based on selection changes.
+		this.listenTo( document.selection, 'change', () => {
 			this.isFocused = document.isFocused && document.selection.editableElement == this;
-		}, { priority: 'high' } );
+		} );
 	}
 }
 

+ 166 - 153
packages/ckeditor5-engine/src/view/element.js

@@ -317,27 +317,6 @@ export default class Element extends Node {
 		return this._attrs.has( key );
 	}
 
-	/**
-	 * Adds or overwrite attribute with a specified key and value.
-	 *
-	 * @param {String} key Attribute key.
-	 * @param {String} value Attribute value.
-	 * @fires module:engine/view/node~Node#change
-	 */
-	setAttribute( key, value ) {
-		value = String( value );
-
-		this._fireChange( 'attributes', this );
-
-		if ( key == 'class' ) {
-			parseClasses( this._classes, value );
-		} else if ( key == 'style' ) {
-			parseInlineStyles( this._styles, value );
-		} else {
-			this._attrs.set( key, value );
-		}
-	}
-
 	/**
 	 * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
 	 * this element.
@@ -369,42 +348,6 @@ export default class Element extends Node {
 		return count;
 	}
 
-	/**
-	 * Removes attribute from the element.
-	 *
-	 * @param {String} key Attribute key.
-	 * @returns {Boolean} Returns true if an attribute existed and has been removed.
-	 * @fires module:engine/view/node~Node#change
-	 */
-	removeAttribute( key ) {
-		this._fireChange( 'attributes', this );
-
-		// Remove class attribute.
-		if ( key == 'class' ) {
-			if ( this._classes.size > 0 ) {
-				this._classes.clear();
-
-				return true;
-			}
-
-			return false;
-		}
-
-		// Remove style attribute.
-		if ( key == 'style' ) {
-			if ( this._styles.size > 0 ) {
-				this._styles.clear();
-
-				return true;
-			}
-
-			return false;
-		}
-
-		// Remove other attributes.
-		return this._attrs.delete( key );
-	}
-
 	/**
 	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
 	 *
@@ -476,34 +419,6 @@ export default class Element extends Node {
 		return true;
 	}
 
-	/**
-	 * Adds specified class.
-	 *
-	 *		element.addClass( 'foo' ); // Adds 'foo' class.
-	 *		element.addClass( 'foo', 'bar' ); // Adds 'foo' and 'bar' classes.
-	 *
-	 * @param {...String} className
-	 * @fires module:engine/view/node~Node#change
-	 */
-	addClass( ...className ) {
-		this._fireChange( 'attributes', this );
-		className.forEach( name => this._classes.add( name ) );
-	}
-
-	/**
-	 * Removes specified class.
-	 *
- 	 *		element.removeClass( 'foo' );  // Removes 'foo' class.
-	 *		element.removeClass( 'foo', 'bar' ); // Removes both 'foo' and 'bar' classes.
-	 *
-	 * @param {...String} className
-	 * @fires module:engine/view/node~Node#change
-	 */
-	removeClass( ...className ) {
-		this._fireChange( 'attributes', this );
-		className.forEach( name => this._classes.delete( name ) );
-	}
-
 	/**
 	 * Returns true if class is present.
 	 * If more then one class is provided - returns true only when all classes are present.
@@ -532,33 +447,6 @@ export default class Element extends Node {
 		return this._classes.keys();
 	}
 
-	/**
-	 * Adds style to the element.
-	 *
-	 *		element.setStyle( 'color', 'red' );
-	 *		element.setStyle( {
-	 *			color: 'red',
-	 *			position: 'fixed'
-	 *		} );
-	 *
-	 * @param {String|Object} property Property name or object with key - value pairs.
-	 * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
-	 * @fires module:engine/view/node~Node#change
-	 */
-	setStyle( property, value ) {
-		this._fireChange( 'attributes', this );
-
-		if ( isPlainObject( property ) ) {
-			const keys = Object.keys( property );
-
-			for ( const key of keys ) {
-				this._styles.set( key, property[ key ] );
-			}
-		} else {
-			this._styles.set( property, value );
-		}
-	}
-
 	/**
 	 * Returns style value for given property.
 	 * Undefined is returned if style does not exist.
@@ -598,20 +486,6 @@ export default class Element extends Node {
 		return true;
 	}
 
-	/**
-	 * Removes specified style.
-	 *
-	 *		element.removeStyle( 'color' );  // Removes 'color' style.
-	 *		element.removeStyle( 'color', 'border-top' ); // Removes both 'color' and 'border-top' styles.
-	 *
-	 * @param {...String} property
-	 * @fires module:engine/view/node~Node#change
-	 */
-	removeStyle( ...property ) {
-		this._fireChange( 'attributes', this );
-		property.forEach( name => this._styles.delete( name ) );
-	}
-
 	/**
 	 * Returns ancestor element that match specified pattern.
 	 * Provided patterns should be compatible with {@link module:engine/view/matcher~Matcher Matcher} as it is used internally.
@@ -636,17 +510,6 @@ export default class Element extends Node {
 		return null;
 	}
 
-	/**
-	 * Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM,
-	 * so they can be used to add special data to elements.
-	 *
-	 * @param {String|Symbol} key
-	 * @param {*} value
-	 */
-	setCustomProperty( key, value ) {
-		this._customProperties.set( key, value );
-	}
-
 	/**
 	 * Returns the custom property value for the given key.
 	 *
@@ -657,16 +520,6 @@ export default class Element extends Node {
 		return this._customProperties.get( key );
 	}
 
-	/**
-	 * Removes the custom property stored under the given key.
-	 *
-	 * @param {String|Symbol} key
-	 * @returns {Boolean} Returns true if property was removed.
-	 */
-	removeCustomProperty( key ) {
-		return this._customProperties.delete( key );
-	}
-
 	/**
 	 * Returns an iterator which iterates over this element's custom properties.
 	 * Iterator provides `[ key, value ]` pairs for each stored property.
@@ -686,12 +539,12 @@ export default class Element extends Node {
  	 *
 	 * For example:
 	 *
-	 *		const element = new ViewElement( 'foo' );
-	 *		element.setAttribute( 'banana', '10' );
-	 *		element.setAttribute( 'apple', '20' );
-	 *		element.setStyle( 'color', 'red' );
-	 *		element.setStyle( 'border-color', 'white' );
-	 *		element.addClass( 'baz' );
+	 *		const element = writer.createContainerElement( 'foo', {
+	 *			banana: '10',
+	 *			apple: '20',
+	 *			style: 'color: red; border-color: white;',
+	 *			class: 'baz'
+	 *		} );
 	 *
 	 *		// returns 'foo class="baz" style="border-color:white;color:red" apple="20" banana="10"'
 	 *		element.getIdentity();
@@ -711,6 +564,166 @@ export default class Element extends Node {
 			( attributes == '' ? '' : ` ${ attributes }` );
 	}
 
+	/**
+	 * Adds or overwrite attribute with a specified key and value.
+	 *
+	 * @protected
+	 * @param {String} key Attribute key.
+	 * @param {String} value Attribute value.
+	 * @fires module:engine/view/node~Node#change
+	 */
+	_setAttribute( key, value ) {
+		value = String( value );
+
+		this._fireChange( 'attributes', this );
+
+		if ( key == 'class' ) {
+			parseClasses( this._classes, value );
+		} else if ( key == 'style' ) {
+			parseInlineStyles( this._styles, value );
+		} else {
+			this._attrs.set( key, value );
+		}
+	}
+
+	/**
+	 * Removes attribute from the element.
+	 *
+	 * @protected
+	 * @param {String} key Attribute key.
+	 * @returns {Boolean} Returns true if an attribute existed and has been removed.
+	 * @fires module:engine/view/node~Node#change
+	 */
+	_removeAttribute( key ) {
+		this._fireChange( 'attributes', this );
+
+		// Remove class attribute.
+		if ( key == 'class' ) {
+			if ( this._classes.size > 0 ) {
+				this._classes.clear();
+
+				return true;
+			}
+
+			return false;
+		}
+
+		// Remove style attribute.
+		if ( key == 'style' ) {
+			if ( this._styles.size > 0 ) {
+				this._styles.clear();
+
+				return true;
+			}
+
+			return false;
+		}
+
+		// Remove other attributes.
+		return this._attrs.delete( key );
+	}
+
+	/**
+	 * Adds specified class.
+	 *
+	 *		element._addClass( 'foo' ); // Adds 'foo' class.
+	 *		element._addClass( [ 'foo', 'bar' ] ); // Adds 'foo' and 'bar' classes.
+	 *
+	 * @protected
+	 * @param {Array.<String>|String} className
+	 * @fires module:engine/view/node~Node#change
+	 */
+	_addClass( className ) {
+		this._fireChange( 'attributes', this );
+
+		className = Array.isArray( className ) ? className : [ className ];
+		className.forEach( name => this._classes.add( name ) );
+	}
+
+	/**
+	 * Removes specified class.
+	 *
+	 *		element._removeClass( 'foo' );  // Removes 'foo' class.
+	 *		element._removeClass( [ 'foo', 'bar' ] ); // Removes both 'foo' and 'bar' classes.
+	 *
+	 * @param {Array.<String>|String} className
+	 * @fires module:engine/view/node~Node#change
+	 */
+	_removeClass( className ) {
+		this._fireChange( 'attributes', this );
+
+		className = Array.isArray( className ) ? className : [ className ];
+		className.forEach( name => this._classes.delete( name ) );
+	}
+
+	/**
+	 * Adds style to the element.
+	 *
+	 *		element._setStyle( 'color', 'red' );
+	 *		element._setStyle( {
+	 *			color: 'red',
+	 *			position: 'fixed'
+	 *		} );
+	 *
+	 * @protected
+	 * @param {String|Object} property Property name or object with key - value pairs.
+	 * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
+	 * @fires module:engine/view/node~Node#change
+	 */
+	_setStyle( property, value ) {
+		this._fireChange( 'attributes', this );
+
+		if ( isPlainObject( property ) ) {
+			const keys = Object.keys( property );
+
+			for ( const key of keys ) {
+				this._styles.set( key, property[ key ] );
+			}
+		} else {
+			this._styles.set( property, value );
+		}
+	}
+
+	/**
+	 * Removes specified style.
+	 *
+	 *		element._removeStyle( 'color' );  // Removes 'color' style.
+	 *		element._removeStyle( [ 'color', 'border-top' ] ); // Removes both 'color' and 'border-top' styles.
+	 *
+	 * @protected
+	 * @param {Array.<String>|String} property
+	 * @fires module:engine/view/node~Node#change
+	 */
+	_removeStyle( property ) {
+		this._fireChange( 'attributes', this );
+
+		property = Array.isArray( property ) ? property : [ property ];
+		property.forEach( name => this._styles.delete( name ) );
+	}
+
+	/**
+	 * Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM,
+	 * so they can be used to add special data to elements.
+	 *
+	 * @protected
+	 * @param {String|Symbol} key
+	 * @param {*} value
+	 */
+	_setCustomProperty( key, value ) {
+		this._customProperties.set( key, value );
+	}
+
+	/**
+	 * Removes the custom property stored under the given key.
+	 *
+	 * @protected
+	 * @param {String|Symbol} key
+	 * @returns {Boolean} Returns true if property was removed.
+	 */
+	_removeCustomProperty( key ) {
+		return this._customProperties.delete( key );
+	}
+
 	/**
 	 * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
 	 *

+ 3 - 3
packages/ckeditor5-engine/src/view/filler.js

@@ -146,10 +146,10 @@ export function isBlockFiller( domNode, blockFiller ) {
  * Assign key observer which move cursor from the end of the inline filler to the beginning of it when
  * the left arrow is pressed, so the filler does not break navigation.
  *
- * @param {module:engine/view/document~Document} document Document instance we should inject quirks handling on.
+ * @param {module:engine/view/view~View} view View controller instance we should inject quirks handling on.
  */
-export function injectQuirksHandling( document ) {
-	document.on( 'keydown', jumpOverInlineFiller );
+export function injectQuirksHandling( view ) {
+	view.document.on( 'keydown', jumpOverInlineFiller );
 }
 
 // Move cursor from the end of the inline filler to the beginning of it when, so the filler does not break navigation.

+ 5 - 5
packages/ckeditor5-engine/src/view/observer/clickobserver.js

@@ -13,14 +13,14 @@ import DomEventObserver from './domeventobserver';
  * {@link module:engine/view/document~Document#event:click Click} event observer.
  *
  * Note that this observer is not available by default. To make it available it needs to be added to
- * {@link module:engine/view/document~Document}
- * by a {@link module:engine/view/document~Document#addObserver} method.
+ * {@link module:engine/view/view~View view controller}
+ * by a {@link module:engine/view/view~View#addObserver} method.
  *
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
 export default class ClickObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		this.domEventType = 'click';
 	}
@@ -37,7 +37,7 @@ export default class ClickObserver extends DomEventObserver {
  *
  * Note that this event is not available by default. To make it available
  * {@link module:engine/view/observer/clickobserver~ClickObserver} needs to be added
- * to {@link module:engine/view/document~Document} by a {@link module:engine/view/document~Document#addObserver} method.
+ * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method.
  *
  * @see module:engine/view/observer/clickobserver~ClickObserver
  * @event module:engine/view/document~Document#event:click

+ 13 - 5
packages/ckeditor5-engine/src/view/observer/domeventdata.js

@@ -16,18 +16,26 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  */
 export default class DomEventData {
 	/**
-	 * @param {module:engine/view/document~Document} document The instance of the tree view Document.
+	 * @param {module:engine/view/view~View} view The instance of the view controller.
 	 * @param {Event} domEvent The DOM event.
 	 * @param {Object} [additionalData] Additional properties that the instance should contain.
 	 */
-	constructor( document, domEvent, additionalData ) {
+	constructor( view, domEvent, additionalData ) {
+		/**
+		 * Instance of the view controller.
+		 *
+		 * @readonly
+		 * @member {module:engine/view/view~View} module:engine/view/observer/observer~Observer.DomEvent#view
+		 */
+		this.view = view;
+
 		/**
 		 * The instance of the document.
 		 *
 		 * @readonly
-		 * @member {module:engine/view/document~Document} module:engine/view/observer/observer~Observer.DomEvent#view
+		 * @member {module:engine/view/document~Document} module:engine/view/observer/observer~Observer.DomEvent#document
 		 */
-		this.document = document;
+		this.document = view.document;
 
 		/**
 		 * The DOM event.
@@ -55,7 +63,7 @@ export default class DomEventData {
 	 * @type module:engine/view/element~Element
 	 */
 	get target() {
-		return this.document.domConverter.mapDomToView( this.domTarget );
+		return this.view.domConverter.mapDomToView( this.domTarget );
 	}
 
 	/**

+ 3 - 3
packages/ckeditor5-engine/src/view/observer/domeventobserver.js

@@ -55,8 +55,8 @@ export default class DomEventObserver extends Observer {
 	/**
 	 * @inheritDoc
 	 */
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		/**
 		 * If set to `true` DOM events will be listened on the capturing phase.
@@ -93,7 +93,7 @@ export default class DomEventObserver extends Observer {
 	 */
 	fire( eventType, domEvent, additionalData ) {
 		if ( this.isEnabled ) {
-			this.document.fire( eventType, new DomEventData( this.document, domEvent, additionalData ) );
+			this.document.fire( eventType, new DomEventData( this.view, domEvent, additionalData ) );
 		}
 	}
 }

+ 6 - 6
packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js

@@ -25,10 +25,10 @@ export default class FakeSelectionObserver extends Observer {
 	/**
 	 * Creates new FakeSelectionObserver instance.
 	 *
-	 * @param {module:engine/view/document~Document} document
+	 * @param {module:engine/view/view~View} view
 	 */
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		/**
 		 * Fires debounced event `selectionChangeDone`. It uses `lodash#debounce` method to delay function call.
@@ -83,16 +83,16 @@ export default class FakeSelectionObserver extends Observer {
 	_handleSelectionMove( keyCode ) {
 		const selection = this.document.selection;
 		const newSelection = new ViewSelection( selection );
-		newSelection.setFake( false );
+		newSelection._setFake( false );
 
 		// Left or up arrow pressed - move selection to start.
 		if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) {
-			newSelection.setTo( newSelection.getFirstPosition() );
+			newSelection._setTo( newSelection.getFirstPosition() );
 		}
 
 		// Right or down arrow pressed - move selection to end.
 		if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) {
-			newSelection.setTo( newSelection.getLastPosition() );
+			newSelection._setTo( newSelection.getLastPosition() );
 		}
 
 		const data = {

+ 8 - 9
packages/ckeditor5-engine/src/view/observer/focusobserver.js

@@ -17,16 +17,17 @@ import DomEventObserver from './domeventobserver';
  * Focus observer handle also {@link module:engine/view/rooteditableelement~RootEditableElement#isFocused isFocused} property of the
  * {@link module:engine/view/rooteditableelement~RootEditableElement root elements}.
  *
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
  *
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
 export default class FocusObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		this.domEventType = [ 'focus', 'blur' ];
 		this.useCapture = true;
+		const document = this.document;
 
 		document.on( 'focus', () => {
 			document.isFocused = true;
@@ -36,7 +37,7 @@ export default class FocusObserver extends DomEventObserver {
 			// overwrite new DOM selection with selection from the view.
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
 			// Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
-			this._renderTimeoutId = setTimeout( () => document.render(), 50 );
+			this._renderTimeoutId = setTimeout( () => view.render(), 50 );
 		} );
 
 		document.on( 'blur', ( evt, data ) => {
@@ -46,7 +47,7 @@ export default class FocusObserver extends DomEventObserver {
 				document.isFocused = false;
 
 				// Re-render the document to update view elements.
-				document.render();
+				view.render();
 			}
 		} );
 
@@ -80,8 +81,7 @@ export default class FocusObserver extends DomEventObserver {
  * Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
  *
  * Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
  *
  * @see module:engine/view/observer/focusobserver~FocusObserver
  * @event module:engine/view/document~Document#event:focus
@@ -94,8 +94,7 @@ export default class FocusObserver extends DomEventObserver {
  * Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
  *
  * Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
  *
  * @see module:engine/view/observer/focusobserver~FocusObserver
  * @event module:engine/view/document~Document#event:blur

+ 5 - 7
packages/ckeditor5-engine/src/view/observer/keyobserver.js

@@ -13,13 +13,13 @@ import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 /**
  * {@link module:engine/view/document~Document#event:keydown Key down} event observer.
  *
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
  *
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
 export default class KeyObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		this.domEventType = [ 'keydown', 'keyup' ];
 	}
@@ -45,8 +45,7 @@ export default class KeyObserver extends DomEventObserver {
  * Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
  *
  * Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
  *
  * @see module:engine/view/observer/keyobserver~KeyObserver
  * @event module:engine/view/document~Document#event:keydown
@@ -59,8 +58,7 @@ export default class KeyObserver extends DomEventObserver {
  * Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
  *
  * Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
  *
  * @see module:engine/view/observer/keyobserver~KeyObserver
  * @event module:engine/view/document~Document#event:keyup

+ 4 - 5
packages/ckeditor5-engine/src/view/observer/mouseobserver.js

@@ -13,14 +13,13 @@ import DomEventObserver from './domeventobserver';
  * Mouse events observer.
  *
  * Note that this observer is not available by default. To make it available it needs to be added to
- * {@link module:engine/view/document~Document}
- * by {@link module:engine/view/document~Document#addObserver} method.
+ * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method.
  *
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
 export default class MouseObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		this.domEventType = 'mousedown';
 	}
@@ -36,7 +35,7 @@ export default class MouseObserver extends DomEventObserver {
  * Introduced by {@link module:engine/view/observer/mouseobserver~MouseObserver}.
  *
  * Note that this event is not available by default. To make it available {@link module:engine/view/observer/mouseobserver~MouseObserver}
- * needs to be added to {@link module:engine/view/document~Document} by a {@link module:engine/view/document~Document#addObserver} method.
+ * needs to be added to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method.
  *
  * @see module:engine/view/observer/mouseobserver~MouseObserver
  * @event module:engine/view/document~Document#event:mousedown

+ 12 - 13
packages/ckeditor5-engine/src/view/observer/mutationobserver.js

@@ -26,13 +26,13 @@ import isEqualWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/isEqualWith';
  * mutations on elements which do not have corresponding view elements. Also
  * {@link module:engine/view/observer/mutationobserver~MutatedText text mutation} is fired only if parent element do not change child list.
  *
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
  *
  * @extends module:engine/view/observer/observer~Observer
  */
 export default class MutationObserver extends Observer {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		/**
 		 * Native mutation observer config.
@@ -48,18 +48,18 @@ export default class MutationObserver extends Observer {
 		};
 
 		/**
-		 * Reference to the {@link module:engine/view/document~Document#domConverter}.
+		 * Reference to the {@link module:engine/view/view~View#domConverter}.
 		 *
 		 * @member {module:engine/view/domconverter~DomConverter}
 		 */
-		this.domConverter = document.domConverter;
+		this.domConverter = view.domConverter;
 
 		/**
-		 * Reference to the {@link module:engine/view/document~Document#renderer}.
+		 * Reference to the {@link module:engine/view/view~View#renderer}.
 		 *
 		 * @member {module:engine/view/renderer~Renderer}
 		 */
-		this.renderer = document.renderer;
+		this.renderer = view._renderer;
 
 		/**
 		 * Observed DOM elements.
@@ -240,8 +240,8 @@ export default class MutationObserver extends Observer {
 			// Anchor and focus has to be properly mapped to view.
 			if ( viewSelectionAnchor && viewSelectionFocus ) {
 				viewSelection = new ViewSelection();
-				viewSelection.setTo( viewSelectionAnchor );
-				viewSelection.setFocus( viewSelectionFocus );
+				viewSelection._setTo( viewSelectionAnchor );
+				viewSelection._setFocus( viewSelectionFocus );
 			}
 		}
 
@@ -249,7 +249,7 @@ export default class MutationObserver extends Observer {
 
 		// If nothing changes on `mutations` event, at this point we have "dirty DOM" (changed) and de-synched
 		// view (which has not been changed). In order to "reset DOM" we render the view again.
-		this.document.render();
+		this.view.render();
 
 		function sameNodes( child1, child2 ) {
 			// First level of comparison (array of children vs array of children) – use the Lodash's default behavior.
@@ -295,14 +295,13 @@ export default class MutationObserver extends Observer {
 }
 
 /**
- * Fired when mutation occurred. If tree view is not changed on this event, DOM will be reverter to the state before
+ * Fired when mutation occurred. If tree view is not changed on this event, DOM will be reverted to the state before
  * mutation, so all changes which should be applied, should be handled on this event.
  *
  * Introduced by {@link module:engine/view/observer/mutationobserver~MutationObserver}.
  *
  * Note that because {@link module:engine/view/observer/mutationobserver~MutationObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
  *
  * @see module:engine/view/observer/mutationobserver~MutationObserver
  * @event module:engine/view/document~Document#event:mutations

+ 14 - 6
packages/ckeditor5-engine/src/view/observer/observer.js

@@ -21,16 +21,24 @@ export default class Observer {
 	/**
 	 * Creates an instance of the observer.
 	 *
-	 * @param {module:engine/view/document~Document} document
+	 * @param {module:engine/view/view~View} view
 	 */
-	constructor( document ) {
+	constructor( view ) {
+		/**
+		 * Instance of the view controller.
+		 *
+		 * @readonly
+		 * @member {module:engine/view/view~View}
+		 */
+		this.view = view;
+
 		/**
 		 * Reference to the {@link module:engine/view/document~Document} object.
 		 *
 		 * @readonly
 		 * @member {module:engine/view/document~Document}
 		 */
-		this.document = document;
+		this.document = view.document;
 
 		/**
 		 * State of the observer. If it is disabled events will not be fired.
@@ -42,8 +50,8 @@ export default class Observer {
 	}
 
 	/**
-	 * Enables the observer. This method is called when then observer is registered to the
-	 * {@link module:engine/view/document~Document} and after {@link module:engine/view/document~Document#render rendering}
+	 * Enables the observer. This method is called when the observer is registered to the
+	 * {@link module:engine/view/view~View} and after {@link module:engine/view/view~View#render rendering}
 	 * (all observers are {@link #disable disabled} before rendering).
 	 *
 	 * A typical use case for disabling observers is that mutation observers need to be disabled for the rendering.
@@ -57,7 +65,7 @@ export default class Observer {
 
 	/**
 	 * Disables the observer. This method is called before
-	 * {@link module:engine/view/document~Document#render rendering} to prevent firing events during rendering.
+	 * {@link module:engine/view/view~View#render rendering} to prevent firing events during rendering.
 	 *
 	 * @see module:engine/view/observer/observer~Observer#enable
 	 */

+ 10 - 20
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -21,14 +21,14 @@ import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
  * {@link module:engine/view/document~Document#event:selectionChange} event only if selection change was the only change in the document
  * and DOM selection is different then the view selection.
  *
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
  *
  * @see module:engine/view/observer/mutationobserver~MutationObserver
  * @extends module:engine/view/observer/observer~Observer
  */
 export default class SelectionObserver extends Observer {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		/**
 		 * Instance of the mutation observer. Selection observer calls
@@ -39,15 +39,7 @@ export default class SelectionObserver extends Observer {
 		 * @member {module:engine/view/observer/mutationobserver~MutationObserver}
 		 * module:engine/view/observer/selectionobserver~SelectionObserver#mutationObserver
 		 */
-		this.mutationObserver = document.getObserver( MutationObserver );
-
-		/**
-		 * Reference to the {@link module:engine/view/document~Document} object.
-		 *
-		 * @readonly
-		 * @member {module:engine/view/document~Document} module:engine/view/observer/selectionobserver~SelectionObserver#document
-		 */
-		this.document = document;
+		this.mutationObserver = view.getObserver( MutationObserver );
 
 		/**
 		 * Reference to the view {@link module:engine/view/selection~Selection} object used to compare new selection with it.
@@ -55,17 +47,17 @@ export default class SelectionObserver extends Observer {
 		 * @readonly
 		 * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#selection
 		 */
-		this.selection = document.selection;
+		this.selection = this.document.selection;
 
 		/* eslint-disable max-len */
 		/**
-		 * Reference to the {@link module:engine/view/document~Document#domConverter}.
+		 * Reference to the {@link module:engine/view/view~View#domConverter}.
 		 *
 		 * @readonly
 		 * @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
 		 */
 		/* eslint-enable max-len */
-		this.domConverter = document.domConverter;
+		this.domConverter = view.domConverter;
 
 		/**
 		 * Set of documents which have added "selectionchange" listener to avoid adding listener twice to the same
@@ -172,7 +164,7 @@ export default class SelectionObserver extends Observer {
 		if ( this.selection.isSimilar( newViewSelection ) ) {
 			// If selection was equal and we are at this point of algorithm, it means that it was incorrect.
 			// Just re-render it, no need to fire any events, etc.
-			this.document.render();
+			this.view.render();
 		} else {
 			const data = {
 				oldSelection: this.selection,
@@ -208,8 +200,7 @@ export default class SelectionObserver extends Observer {
  * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  *
  * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
  *
  * @see module:engine/view/observer/selectionobserver~SelectionObserver
  * @event module:engine/view/document~Document#event:selectionChange
@@ -226,8 +217,7 @@ export default class SelectionObserver extends Observer {
  * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  *
  * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
  *
  * @see module:engine/view/observer/selectionobserver~SelectionObserver
  * @event module:engine/view/document~Document#event:selectionChangeDone

+ 35 - 29
packages/ckeditor5-engine/src/view/placeholder.js

@@ -9,7 +9,6 @@
 
 import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import '../../theme/placeholder.css';
 
 const listener = {};
@@ -27,72 +26,71 @@ const documentPlaceholders = new WeakMap();
  * @param {Function} [checkFunction] If provided it will be called before checking if placeholder should be displayed.
  * If function returns `false` placeholder will not be showed.
  */
-export function attachPlaceholder( element, placeholderText, checkFunction ) {
-	const document = element.document;
-
-	if ( !document ) {
-		/**
-		 * Provided element is not placed in any {@link module:engine/view/document~Document}.
-		 *
-		 * @error view-placeholder-element-is-detached
-		 */
-		throw new CKEditorError( 'view-placeholder-element-is-detached: Provided element is not placed in document.' );
-	}
+export function attachPlaceholder( view, element, placeholderText, checkFunction ) {
+	const document = view.document;
 
 	// Detach placeholder if was used before.
-	detachPlaceholder( element );
+	detachPlaceholder( view, element );
 
 	// Single listener per document.
 	if ( !documentPlaceholders.has( document ) ) {
 		documentPlaceholders.set( document, new Map() );
-		listener.listenTo( document, 'render', () => updateAllPlaceholders( document ), { priority: 'high' } );
+
+		// Attach listener just before rendering and update placeholders.
+		listener.listenTo( view, 'render', () => updateAllPlaceholders( view ) );
 	}
 
 	// Store text in element's data attribute.
 	// This data attribute is used in CSS class to show the placeholder.
-	element.setAttribute( 'data-placeholder', placeholderText );
+	view.change( writer => {
+		writer.setAttribute( 'data-placeholder', placeholderText, element );
+	} );
 
 	// Store information about placeholder.
 	documentPlaceholders.get( document ).set( element, checkFunction );
 
 	// Update right away too.
-	updateSinglePlaceholder( element, checkFunction );
+	updateSinglePlaceholder( view, element, checkFunction );
 }
 
 /**
  * Removes placeholder functionality from given element.
  *
+ * @param {module:engine/view/view~View} view
  * @param {module:engine/view/element~Element} element
  */
-export function detachPlaceholder( element ) {
+export function detachPlaceholder( view, element ) {
 	const document = element.document;
 
-	element.removeClass( 'ck-placeholder' );
-	element.removeAttribute( 'data-placeholder' );
-
 	if ( documentPlaceholders.has( document ) ) {
 		documentPlaceholders.get( document ).delete( element );
 	}
+
+	view.change( writer => {
+		writer.removeClass( 'ck-placeholder', element );
+		writer.removeAttribute( 'data-placeholder', element );
+	} );
 }
 
 // Updates all placeholders of given document.
 //
 // @private
-// @param {module:engine/view/document~Document} document
-function updateAllPlaceholders( document ) {
-	const placeholders = documentPlaceholders.get( document );
+// @param {module:engine/view/view~View} view
+function updateAllPlaceholders( view ) {
+	const placeholders = documentPlaceholders.get( view.document );
 
 	for ( const [ element, checkFunction ] of placeholders ) {
-		updateSinglePlaceholder( element, checkFunction );
+		updateSinglePlaceholder( view, element, checkFunction );
 	}
 }
 
 // Updates placeholder class of given element.
 //
 // @private
+// @param {module:engine/view/view~View} view
 // @param {module:engine/view/element~Element} element
 // @param {Function} checkFunction
-function updateSinglePlaceholder( element, checkFunction ) {
+function updateSinglePlaceholder( view, element, checkFunction ) {
 	const document = element.document;
 
 	// Element was removed from document.
@@ -105,7 +103,9 @@ function updateSinglePlaceholder( element, checkFunction ) {
 
 	// If checkFunction is provided and returns false - remove placeholder.
 	if ( checkFunction && !checkFunction() ) {
-		element.removeClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.removeClass( 'ck-placeholder', element );
+		} );
 
 		return;
 	}
@@ -116,15 +116,21 @@ function updateSinglePlaceholder( element, checkFunction ) {
 
 	// If element is empty and editor is blurred.
 	if ( !document.isFocused && isEmptyish ) {
-		element.addClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.addClass( 'ck-placeholder', element );
+		} );
 
 		return;
 	}
 
 	// It there are no child elements and selection is not placed inside element.
 	if ( isEmptyish && anchor && anchor.parent !== element ) {
-		element.addClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.addClass( 'ck-placeholder', element );
+		} );
 	} else {
-		element.removeClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.removeClass( 'ck-placeholder', element );
+		} );
 	}
 }

+ 1 - 1
packages/ckeditor5-engine/src/view/rooteditableelement.js

@@ -53,7 +53,7 @@ export default class RootEditableElement extends EditableElement {
 	}
 
 	set rootName( rootName ) {
-		this.setCustomProperty( rootNameSymbol, rootName );
+		this._setCustomProperty( rootNameSymbol, rootName );
 	}
 
 	/**

+ 43 - 40
packages/ckeditor5-engine/src/view/selection.js

@@ -21,7 +21,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  * Class representing selection in tree view.
  *
  * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using
- * {@link module:engine/view/selection~Selection#setTo} method.
+ * {@link module:engine/view/selection~Selection#_setTo} method.
  * That method create copies of provided ranges and store those copies internally. Further modifications to passed
  * ranges will not change selection's state.
  * Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges},
@@ -97,34 +97,14 @@ export default class Selection {
 		this._fakeSelectionLabel = '';
 
 		if ( selectable ) {
-			this.setTo( selectable, backwardSelectionOrOffset );
+			this._setTo( selectable, backwardSelectionOrOffset );
 		}
 	}
 
-	/**
-	 * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection
-	 * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to
-	 * the user and selection over elements can be represented in other way, for example by applying proper CSS class.
-	 *
-	 * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be
-	 * properly handled by screen readers).
-	 *
-	 * @fires change
-	 * @param {Boolean} [value=true] If set to true selection will be marked as `fake`.
-	 * @param {Object} [options] Additional options.
-	 * @param {String} [options.label=''] Fake selection label.
-	 */
-	setFake( value = true, options = {} ) {
-		this._isFake = value;
-		this._fakeSelectionLabel = value ? options.label || '' : '';
-
-		this.fire( 'change' );
-	}
-
 	/**
 	 * Returns true if selection instance is marked as `fake`.
 	 *
-	 * @see #setFake
+	 * @see #_setFake
 	 * @returns {Boolean}
 	 */
 	get isFake() {
@@ -134,7 +114,7 @@ export default class Selection {
 	/**
 	 * Returns fake selection label.
 	 *
-	 * @see #setFake
+	 * @see #_setFake
 	 * @returns {String}
 	 */
 	get fakeSelectionLabel() {
@@ -389,6 +369,25 @@ export default class Selection {
 		return true;
 	}
 
+	/**
+	 * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only
+	 * one range in the selection, and that range contains exactly one element.
+	 * Returns `null` if there is no selected element.
+	 *
+	 * @returns {module:engine/view/element~Element|null}
+	 */
+	getSelectedElement() {
+		if ( this.rangeCount !== 1 ) {
+			return null;
+		}
+
+		const range = this.getFirstRange();
+		const nodeAfterStart = range.start.nodeAfter;
+		const nodeBeforeEnd = range.end.nodeBefore;
+
+		return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
+	}
+
 	/**
 	 * Removes all ranges that were added to the selection.
 	 *
@@ -429,12 +428,13 @@ export default class Selection {
 	 *
 	 * 		// Removes all ranges.
 	 *		selection.setTo( null );
-
+	 *
+	 * @protected
 	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
 	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
 	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
 	 */
-	setTo( selectable, backwardSelectionOrOffset ) {
+	_setTo( selectable, backwardSelectionOrOffset ) {
 		if ( selectable === null ) {
 			this._removeAllRanges();
 		} else if ( selectable instanceof Selection ) {
@@ -489,12 +489,13 @@ export default class Selection {
 	 *
 	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
 	 *
+	 * @protected
 	 * @fires change
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
 	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
-	setFocus( itemOrPosition, offset ) {
+	_setFocus( itemOrPosition, offset ) {
 		if ( this.anchor === null ) {
 			/**
 			 * Cannot set selection focus if there are no ranges in selection.
@@ -526,22 +527,24 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only
-	 * one range in the selection, and that range contains exactly one element.
-	 * Returns `null` if there is no selected element.
+	 * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection
+	 * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to
+	 * the user and selection over elements can be represented in other way, for example by applying proper CSS class.
 	 *
-	 * @returns {module:engine/view/element~Element|null}
+	 * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be
+	 * properly handled by screen readers).
+	 *
+	 * @protected
+	 * @fires change
+	 * @param {Boolean} [value=true] If set to true selection will be marked as `fake`.
+	 * @param {Object} [options] Additional options.
+	 * @param {String} [options.label=''] Fake selection label.
 	 */
-	getSelectedElement() {
-		if ( this.rangeCount !== 1 ) {
-			return null;
-		}
-
-		const range = this.getFirstRange();
-		const nodeAfterStart = range.start.nodeAfter;
-		const nodeBeforeEnd = range.end.nodeBefore;
+	_setFake( value = true, options = {} ) {
+		this._isFake = value;
+		this._fakeSelectionLabel = value ? options.label || '' : '';
 
-		return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
+		this.fire( 'change' );
 	}
 
 	/**

+ 3 - 3
packages/ckeditor5-engine/src/view/uielement.js

@@ -110,10 +110,10 @@ export default class UIElement extends Element {
  * The callback handles the situation when right arrow key is pressed and selection is collapsed before a UI element.
  * Without this handler, it would be impossible to "jump over" UI element using right arrow key.
  *
- * @param {module:engine/view/document~Document} document Document to which the quirks handling will be injected.
+ * @param {module:engine/view/view~View} view View controller to which the quirks handling will be injected.
  */
-export function injectUiElementHandling( document ) {
-	document.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, document.domConverter ) );
+export function injectUiElementHandling( view ) {
+	view.document.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, view.domConverter ) );
 }
 
 // Returns `null` because block filler is not needed for UIElements.

+ 415 - 0
packages/ckeditor5-engine/src/view/view.js

@@ -0,0 +1,415 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/view
+ */
+
+import Document from './document';
+import Writer from './writer';
+import Renderer from './renderer';
+import DomConverter from './domconverter';
+
+import MutationObserver from './observer/mutationobserver';
+import KeyObserver from './observer/keyobserver';
+import FakeSelectionObserver from './observer/fakeselectionobserver';
+import SelectionObserver from './observer/selectionobserver';
+import FocusObserver from './observer/focusobserver';
+
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
+import { injectUiElementHandling } from './uielement';
+import { injectQuirksHandling } from './filler';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Editor's view controller class. Its main responsibility is DOM - View management for editing purposes, to provide
+ * abstraction over the DOM structure and events and hide all browsers quirks.
+ *
+ * View controller renders view document to DOM whenever view structure changes. To determine when view can be rendered,
+ * all changes need to be done using the {@link module:engine/view/view~View#change} method, using
+ * {@link module:engine/view/writer~Writer}:
+ *
+ *		view.change( writer => {
+ *			writer.insert( position, writer.createText( 'foo' ) );
+ *		} );
+ *
+ * View controller also register {@link module:engine/view/observer/observer~Observer observers} which observes changes
+ * on DOM and fire events on the {@link module:engine/view/document~Document Document}.
+ * Note that the following observers are added by the class constructor and are always available:
+ *
+ * * {@link module:engine/view/observer/mutationobserver~MutationObserver},
+ * * {@link module:engine/view/observer/selectionobserver~SelectionObserver},
+ * * {@link module:engine/view/observer/focusobserver~FocusObserver},
+ * * {@link module:engine/view/observer/keyobserver~KeyObserver},
+ * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
+ *
+ * This class also {@link module:engine/view/view~View#attachDomRoot bind DOM and View elements}.
+ *
+ * If you do not need full DOM - View management, and want to only transform the tree of view elements to the DOM
+ * elements you do not need this controller, you can use the {@link module:engine/view/domconverter~DomConverter DomConverter}.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
+ */
+export default class View {
+	constructor() {
+		/**
+		 * Instance of the {@link module:engine/view/document~Document} associated with this view controller.
+		 *
+		 * @readonly
+		 * @member {module:engine/view/document~Document} module:engine/view/view~View#document
+		 */
+		this.document = new Document();
+
+		/**
+		 * Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by
+		 * {@link module:engine/view/view~View#renderer renderer}
+		 * and {@link module:engine/view/observer/observer~Observer observers}.
+		 *
+		 * @readonly
+		 * @member {module:engine/view/domconverter~DomConverter} module:engine/view/view~View#domConverter
+		 */
+		this.domConverter = new DomConverter();
+
+		/**
+		 * Instance of the {@link module:engine/view/renderer~Renderer renderer}.
+		 *
+		 * @protected
+		 * @member {module:engine/view/renderer~Renderer} module:engine/view/view~View#renderer
+		 */
+		this._renderer = new Renderer( this.domConverter, this.document.selection );
+		this._renderer.bind( 'isFocused' ).to( this.document );
+
+		/**
+		 * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
+		 *
+		 * @readonly
+		 * @member {Map} module:engine/view/view~View#domRoots
+		 */
+		this.domRoots = new Map();
+
+		/**
+		 * Map of registered {@link module:engine/view/observer/observer~Observer observers}.
+		 *
+		 * @private
+		 * @member {Map.<Function, module:engine/view/observer/observer~Observer>} module:engine/view/view~View#_observers
+		 */
+		this._observers = new Map();
+
+		/**
+		 * Is set to `true` when {@link #change view changes} are currently in progress.
+		 *
+		 * @private
+		 * @member {Boolean} module:engine/view/view~View#_ongoingChange
+		 */
+		this._ongoingChange = false;
+
+		/**
+		 * Is set to `true` when rendering view to DOM was started.
+		 * This is used to check whether view document can accept changes in current state.
+		 * From the moment when rendering to DOM is stared view tree is locked to prevent changes that will not be
+		 * reflected in the DOM.
+		 *
+		 * @private
+		 * @member {Boolean} module:engine/view/view~View#_renderingStarted
+		 */
+		this._renderingStarted = false;
+
+		/**
+		 * Writer instance used in {@link #change change method) callbacks.
+		 *
+		 * @private
+		 * @member {module:engine/view/writer~Writer} module:engine/view/view~View#_writer
+		 */
+		this._writer = new Writer( this.document );
+
+		// Add default observers.
+		this.addObserver( MutationObserver );
+		this.addObserver( SelectionObserver );
+		this.addObserver( FocusObserver );
+		this.addObserver( KeyObserver );
+		this.addObserver( FakeSelectionObserver );
+
+		// Inject quirks handlers.
+		injectQuirksHandling( this );
+		injectUiElementHandling( this );
+
+		// Use 'low` priority so that all listeners on 'normal` priority will be executed before.
+		this.on( 'render', () => {
+			this._render();
+		}, { priority: 'low' } );
+	}
+
+	/**
+	 * Attaches DOM root element to the view element and enable all observers on that element.
+	 * Also {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view
+	 * what means that all child nodes will be removed and replaced with content of the view root.
+	 *
+	 * This method also will change view element name as the same as tag name of given dom root.
+	 * Name is always transformed to lower case.
+	 *
+	 * @param {Element} domRoot DOM root element.
+	 * @param {String} [name='main'] Name of the root.
+	 */
+	attachDomRoot( domRoot, name = 'main' ) {
+		const viewRoot = this.document.getRoot( name );
+
+		// Set view root name the same as DOM root tag name.
+		viewRoot._name = domRoot.tagName.toLowerCase();
+
+		this.domRoots.set( name, domRoot );
+		this.domConverter.bindElements( domRoot, viewRoot );
+		this._renderer.markToSync( 'children', viewRoot );
+		this._renderer.domDocuments.add( domRoot.ownerDocument );
+
+		viewRoot.on( 'change:children', ( evt, node ) => this._renderer.markToSync( 'children', node ) );
+		viewRoot.on( 'change:attributes', ( evt, node ) => this._renderer.markToSync( 'attributes', node ) );
+		viewRoot.on( 'change:text', ( evt, node ) => this._renderer.markToSync( 'text', node ) );
+
+		for ( const observer of this._observers.values() ) {
+			observer.observe( domRoot, name );
+		}
+	}
+
+	/**
+	 * Gets DOM root element.
+	 *
+	 * @param {String} [name='main']  Name of the root.
+	 * @returns {Element} DOM root element instance.
+	 */
+	getDomRoot( name = 'main' ) {
+		return this.domRoots.get( name );
+	}
+
+	/**
+	 * Creates observer of the given type if not yet created, {@link module:engine/view/observer/observer~Observer#enable enables} it
+	 * and {@link module:engine/view/observer/observer~Observer#observe attaches} to all existing and future
+	 * {@link #domRoots DOM roots}.
+	 *
+	 * Note: Observers are recognized by their constructor (classes). A single observer will be instantiated and used only
+	 * when registered for the first time. This means that features and other components can register a single observer
+	 * multiple times without caring whether it has been already added or not.
+	 *
+	 * @param {Function} Observer The constructor of an observer to add.
+	 * Should create an instance inheriting from {@link module:engine/view/observer/observer~Observer}.
+	 * @returns {module:engine/view/observer/observer~Observer} Added observer instance.
+	 */
+	addObserver( Observer ) {
+		let observer = this._observers.get( Observer );
+
+		if ( observer ) {
+			return observer;
+		}
+
+		observer = new Observer( this );
+
+		this._observers.set( Observer, observer );
+
+		for ( const [ name, domElement ] of this.domRoots ) {
+			observer.observe( domElement, name );
+		}
+
+		observer.enable();
+
+		return observer;
+	}
+
+	/**
+	 * Returns observer of the given type or `undefined` if such observer has not been added yet.
+	 *
+	 * @param {Function} Observer The constructor of an observer to get.
+	 * @returns {module:engine/view/observer/observer~Observer|undefined} Observer instance or undefined.
+	 */
+	getObserver( Observer ) {
+		return this._observers.get( Observer );
+	}
+
+	/**
+	 * Disables all added observers.
+	 */
+	disableObservers() {
+		for ( const observer of this._observers.values() ) {
+			observer.disable();
+		}
+	}
+
+	/**
+	 * Enables all added observers.
+	 */
+	enableObservers() {
+		for ( const observer of this._observers.values() ) {
+			observer.enable();
+		}
+	}
+
+	/**
+	 * Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
+	 * caret, if not already visible to the user.
+	 */
+	scrollToTheSelection() {
+		const range = this.document.selection.getFirstRange();
+
+		if ( range ) {
+			scrollViewportToShowTarget( {
+				target: this.domConverter.viewRangeToDom( range ),
+				viewportOffset: 20
+			} );
+		}
+	}
+
+	/**
+	 * It will focus DOM element representing {@link module:engine/view/editableelement~EditableElement EditableElement}
+	 * that is currently having selection inside.
+	 */
+	focus() {
+		if ( !this.document.isFocused ) {
+			const editable = this.document.selection.editableElement;
+
+			if ( editable ) {
+				this.domConverter.focus( editable );
+				this.render();
+			} else {
+				/**
+				 * Before focusing view document, selection should be placed inside one of the view's editables.
+				 * Normally its selection will be converted from model document (which have default selection), but
+				 * when using view document on its own, we need to manually place selection before focusing it.
+				 *
+				 * @error view-focus-no-selection
+				 */
+				log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
+			}
+		}
+	}
+
+	/**
+	 * Change method is the primary way of changing the view. You should use it to modify any node in the view tree.
+	 * It makes sure that after all changes are made view is rendered to DOM. It prevents situations when DOM is updated
+	 * when view state is not yet correct. It allows to nest calls one inside another and still perform single rendering
+	 * after all changes are applied.
+	 *
+	 *		view.change( writer => {
+	 *			writer.insert( position1, writer.createText( 'foo' ) );
+	 *
+	 *			view.change( writer => {
+	 *				writer.insert( position2, writer.createText( 'bar' ) );
+	 *			} );
+	 *
+	 * 			writer.remove( range );
+	 *		} );
+	 *
+	 * Change block is executed immediately.
+	 *
+	 * When the outermost change block is done and rendering to DOM is over it fires
+	 * {@link module:engine/view/view~View#event:render} event.
+	 *
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when
+	 * change block is used after rendering to DOM has started.
+	 *
+	 * @param {Function} callback Callback function which may modify the view.
+	 */
+	change( callback ) {
+		// Check if change is performed in correct moment.
+		this._assertRenderingInProgress();
+
+		// If other changes are in progress wait with rendering until every ongoing change is over.
+		if ( this._ongoingChange ) {
+			callback( this._writer );
+		} else {
+			this._ongoingChange = true;
+
+			callback( this._writer );
+			this.fire( 'render' );
+
+			this._ongoingChange = false;
+			this._renderingStarted = false;
+		}
+	}
+
+	/**
+	 * Renders {@link module:engine/view/document~Document view document} to DOM. If any view changes are
+	 * currently in progress, rendering will start after all {@link #change change blocks} are processed.
+	 *
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when
+	 * trying to re-render when rendering to DOM has already started.
+	 */
+	render() {
+		// Check if rendering is performed in correct moment.
+		this._assertRenderingInProgress();
+
+		// Render only if no ongoing changes are in progress. If there are some, view document will be rendered after all
+		// changes are done. This way view document will not be rendered in the middle of some changes.
+		if ( !this._ongoingChange ) {
+			this.fire( 'render' );
+			this._renderingStarted = false;
+		}
+	}
+
+	/**
+	 * Destroys this instance. Makes sure that all observers are destroyed and listeners removed.
+	 */
+	destroy() {
+		for ( const observer of this._observers.values() ) {
+			observer.destroy();
+		}
+
+		this.stopListening();
+	}
+
+	/**
+	 * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
+	 * before rendering and re-enabled after that.
+	 *
+	 * @private
+	 */
+	_render() {
+		this._renderingStarted = true;
+
+		this.disableObservers();
+		this._renderer.render();
+		this.enableObservers();
+	}
+
+	/**
+	 * Throws `applying-view-changes-on-rendering` error when trying to modify or re-render view tree when rendering is
+	 * already started
+	 *
+	 * @private
+	 */
+	_assertRenderingInProgress() {
+		if ( this._renderingStarted ) {
+			/**
+			 * There is an attempt to make changes in the view tree after the rendering process
+			 * has started. This may cause unexpected behaviour and inconsistency between the DOM and the view.
+			 * This may be caused by:
+			 *   * calling `view.change()` or `view.render()` methods during rendering process,
+			 *   * calling `view.change()` or `view.render()` methods in callbacks to
+			 *   {module:engine/view/document~Document#event:change view document change event) on `low` priority, after
+			 *   rendering is over for current `change` block.
+			 *
+			 * @error applying-view-changes-on-rendering
+			 */
+			throw new CKEditorError(
+				'applying-view-changes-on-rendering: ' +
+				'Attempting to make changes in the view during rendering process. ' +
+				'This may cause some unexpected behaviour and inconsistency between the DOM and the view.'
+			);
+		}
+	}
+
+	/**
+	 * Fired after a topmost {@link module:engine/view/view~View#change change block} is finished and the DOM rendering has
+	 * been executed.
+	 *
+	 * Actual rendering is performed on 'low' priority. This means that all listeners on 'normal' and above priorities
+	 * will be executed after changes made to view tree but before rendering to the DOM. Use `low` priority for callbacks that
+	 * should be executed after rendering to the DOM.
+	 *
+	 * @event module:engine/view/view~View#event:render
+	 */
+}
+
+mix( View, ObservableMixin );

+ 0 - 8
packages/ckeditor5-engine/src/view/view.jsdoc

@@ -1,8 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/view/view
- */

Plik diff jest za duży
+ 1087 - 557
packages/ckeditor5-engine/src/view/writer.js


+ 10 - 10
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -9,7 +9,7 @@ import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 
 import EditingController from '../../src/controller/editingcontroller';
 
-import ViewDocument from '../../src/view/document';
+import View from '../../src/view/view';
 
 import Mapper from '../../src/conversion/mapper';
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
@@ -39,7 +39,7 @@ describe( 'EditingController', () => {
 
 		it( 'should create controller with properties', () => {
 			expect( editing ).to.have.property( 'model' ).that.equals( model );
-			expect( editing ).to.have.property( 'view' ).that.is.instanceof( ViewDocument );
+			expect( editing ).to.have.property( 'view' ).that.is.instanceof( View );
 			expect( editing ).to.have.property( 'mapper' ).that.is.instanceof( Mapper );
 			expect( editing ).to.have.property( 'downcastDispatcher' ).that.is.instanceof( DowncastDispatcher );
 
@@ -57,15 +57,15 @@ describe( 'EditingController', () => {
 
 		it( 'should bind view roots to model roots', () => {
 			expect( model.document.roots ).to.length( 1 ); // $graveyard
-			expect( editing.view.roots ).to.length( 0 );
+			expect( editing.view.document.roots ).to.length( 0 );
 
 			const modelRoot = model.document.createRoot();
 
 			expect( model.document.roots ).to.length( 2 );
-			expect( editing.view.roots ).to.length( 1 );
-			expect( editing.view.getRoot().document ).to.equal( editing.view );
+			expect( editing.view.document.roots ).to.length( 1 );
+			expect( editing.view.document.getRoot().document ).to.equal( editing.view.document );
 
-			expect( editing.view.getRoot().name ).to.equal( modelRoot.name ).to.equal( '$root' );
+			expect( editing.view.document.getRoot().name ).to.equal( modelRoot.name ).to.equal( '$root' );
 		} );
 	} );
 
@@ -85,7 +85,7 @@ describe( 'EditingController', () => {
 
 			document.body.appendChild( domRoot );
 
-			viewRoot = editing.view.getRoot();
+			viewRoot = editing.view.document.getRoot();
 			editing.view.attachDomRoot( domRoot );
 
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
@@ -169,7 +169,7 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should convert selection from view to model', done => {
-			listener.listenTo( editing.view, 'selectionChange', () => {
+			listener.listenTo( editing.view.document, 'selectionChange', () => {
 				setTimeout( () => {
 					expect( getModelData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>' +
@@ -178,10 +178,10 @@ describe( 'EditingController', () => {
 					);
 
 					done();
-				} );
+				}, 1 );
 			} );
 
-			editing.view.isFocused = true;
+			editing.view.document.isFocused = true;
 			editing.view.render();
 
 			const domSelection = document.getSelection();

+ 27 - 66
packages/ckeditor5-engine/tests/conversion/downcast-converters.js

@@ -39,9 +39,9 @@ describe( 'downcast-helpers', () => {
 
 		// Set name of view root the same as dom root.
 		// This is a mock of attaching view root to dom root.
-		controller.view.getRoot()._name = 'div';
+		controller.view.document.getRoot()._name = 'div';
 
-		viewRoot = controller.view.getRoot();
+		viewRoot = controller.view.document.getRoot();
 
 		conversion = new Conversion();
 		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
@@ -73,21 +73,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<foo></foo>' );
 		} );
 
-		it( 'config.view is an element instance', () => {
-			const helper = downcastElementToElement( {
-				model: 'paragraph',
-				view: new ViewContainerElement( 'p' )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertElement( 'paragraph', modelRoot, 0 );
-			} );
-
-			expectResult( '<p></p>' );
-		} );
-
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastElementToElement( {
 				model: 'fancyParagraph',
@@ -148,20 +133,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<b>foo</b>' );
 		} );
 
-		it( 'config.view is an element instance', () => {
-			const helper = downcastAttributeToElement( 'bold', {
-				view: new ViewAttributeElement( 'strong' )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
-			} );
-
-			expectResult( '<strong>foo</strong>' );
-		} );
-
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastAttributeToElement( 'bold', {
 				view: {
@@ -421,22 +392,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( 'f<search></search>o<search></search>o' );
 		} );
 
-		it( 'config.view is an element instance', () => {
-			const helper = downcastMarkerToElement( {
-				model: 'search',
-				view: new ViewUIElement( 'span', { 'data-marker': 'search' } )
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', modelRoot, 0 );
-				writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
-			} );
-
-			expectResult( 'f<span data-marker="search"></span>o<span data-marker="search"></span>o' );
-		} );
-
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastMarkerToElement( {
 				model: 'search',
@@ -543,14 +498,20 @@ describe( 'downcast-converters', () => {
 
 		controller = new EditingController( model );
 
-		viewRoot = controller.view.getRoot();
+		viewRoot = controller.view.document.getRoot();
 		// Set name of view root the same as dom root.
 		// This is a mock of attaching view root to dom root.
-		controller.view.getRoot()._name = 'div';
+		controller.view.document.getRoot()._name = 'div';
 
 		dispatcher = controller.downcastDispatcher;
 
-		dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
+		dispatcher.on(
+			'insert:paragraph',
+			insertElement(
+				( modelItem, consumable, conversionApi ) => conversionApi.writer.createContainerElement( 'p' )
+			)
+		);
+
 		dispatcher.on( 'attribute:class', changeAttribute() );
 
 		modelRootStart = ModelPosition.createAt( modelRoot, 0 );
@@ -685,7 +646,7 @@ describe( 'downcast-converters', () => {
 				return { key: 'class', value };
 			};
 
-			dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
+			dispatcher.on( 'insert:div', insertElement( ( model, consumable, api ) => api.writer.createContainerElement( 'div' ) ) );
 			dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
 
 			const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
@@ -729,9 +690,9 @@ describe( 'downcast-converters', () => {
 	describe( 'wrap', () => {
 		it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
@@ -749,9 +710,9 @@ describe( 'downcast-converters', () => {
 		it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
 
-			const elementGenerator = value => {
+			const elementGenerator = ( value, data, consumable, api ) => {
 				if ( value == 'bold' ) {
-					return new ViewAttributeElement( 'b' );
+					return api.writer.createAttributeElement( 'b' );
 				}
 			};
 
@@ -777,7 +738,7 @@ describe( 'downcast-converters', () => {
 				new ModelText( 'x' )
 			] );
 
-			const elementGenerator = href => new ViewAttributeElement( 'a', { href } );
+			const elementGenerator = ( href, data, consumable, api ) => api.writer.createAttributeElement( 'a', { href } );
 
 			dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
 
@@ -797,9 +758,9 @@ describe( 'downcast-converters', () => {
 
 		it( 'should support unicode', () => {
 			const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
@@ -816,9 +777,9 @@ describe( 'downcast-converters', () => {
 
 		it( 'should be possible to override wrap', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' );
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 			dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
 				consumable.consume( data.item, 'attribute:bold' );
 			}, { priority: 'high' } );
@@ -1341,12 +1302,12 @@ describe( 'downcast-converters', () => {
 				dispatcher.on( 'insert:div', insertElement( () => {
 					const viewContainer = new ViewContainerElement( 'div' );
 
-					viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
-						element.addClass( descriptor.class );
+					viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => {
+						writer.addClass( descriptor.class, element );
 					} );
 
-					viewContainer.setCustomProperty( 'removeHighlight', element => {
-						element.setAttribute( 'class', '' );
+					viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => {
+						writer.setAttribute( 'class', '', element );
 					} );
 
 					return viewContainer;
@@ -1418,12 +1379,12 @@ describe( 'downcast-converters', () => {
 				dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) );
 				dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) );
 
-				viewDiv.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
+				viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => {
 					expect( descriptor.priority ).to.equal( 10 );
 					expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
 				} );
 
-				viewDiv.setCustomProperty( 'removeHighlight', ( element, id ) => {
+				viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => {
 					expect( id ).to.equal( 'marker:foo-bar-baz' );
 				} );
 

+ 73 - 60
packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js

@@ -8,11 +8,8 @@ import ModelElement from '../../src/model/element';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
 
-import ViewDocument from '../../src/view/document';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
+import View from '../../src/view/view';
 import ViewUIElement from '../../src/view/uielement';
-import { mergeAttributes } from '../../src/view/writer';
 
 import Mapper from '../../src/conversion/mapper';
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
@@ -37,7 +34,7 @@ import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
 describe( 'downcast-selection-converters', () => {
-	let dispatcher, mapper, model, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
+	let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
 
 	beforeEach( () => {
 		model = new Model();
@@ -47,7 +44,8 @@ describe( 'downcast-selection-converters', () => {
 
 		model.schema.extend( '$text', { allowIn: '$root' } );
 
-		viewDoc = new ViewDocument();
+		view = new View();
+		viewDoc = view.document;
 		viewRoot = createViewRoot( viewDoc );
 		viewSelection = viewDoc.selection;
 
@@ -56,10 +54,12 @@ describe( 'downcast-selection-converters', () => {
 
 		highlightDescriptor = { class: 'marker', priority: 1 };
 
-		dispatcher = new DowncastDispatcher( model, { mapper, viewSelection } );
+		dispatcher = new DowncastDispatcher( { mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
+
+		const strongCreator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'strong' );
+		dispatcher.on( 'attribute:bold', wrap( strongCreator ) );
 
 		dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
 		dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
@@ -72,7 +72,7 @@ describe( 'downcast-selection-converters', () => {
 	} );
 
 	afterEach( () => {
-		viewDoc.destroy();
+		view.destroy();
 	} );
 
 	describe( 'default converters', () => {
@@ -206,11 +206,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 				// Convert model to view.
-				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
-				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
-
-				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( docSelection, markers );
+				view.change( writer => {
+					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal(
@@ -231,11 +231,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 				// Convert model to view.
-				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
-				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
-
-				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( docSelection, markers );
+				view.change( writer => {
+					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -258,11 +258,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 				// Convert model to view.
-				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
-				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
-
-				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( docSelection, markers );
+				view.change( writer => {
+					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -283,11 +283,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 				// Convert model to view.
-				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
-				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
-
-				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( docSelection, markers );
+				view.change( writer => {
+					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -310,7 +310,9 @@ describe( 'downcast-selection-converters', () => {
 				} );
 
 				// Convert model to view.
-				dispatcher.convertSelection( docSelection, [] );
+				view.change( writer => {
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -327,13 +329,15 @@ describe( 'downcast-selection-converters', () => {
 				} );
 
 				// Convert model to view.
-				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
+				view.change( writer => {
+					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
 
-				// Add ui element to view.
-				const uiElement = new ViewUIElement( 'span' );
-				viewRoot.insertChildren( 1, uiElement );
+					// Add ui element to view.
+					const uiElement = new ViewUIElement( 'span' );
+					viewRoot.insertChildren( 1, uiElement );
 
-				dispatcher.convertSelection( docSelection, [] );
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -350,13 +354,14 @@ describe( 'downcast-selection-converters', () => {
 				} );
 
 				// Convert model to view.
-				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
+				view.change( writer => {
+					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
 
-				// Add ui element to view.
-				const uiElement = new ViewUIElement( 'span' );
-				viewRoot.insertChildren( 1, uiElement );
-
-				dispatcher.convertSelection( docSelection, [] );
+					// Add ui element to view.
+					const uiElement = new ViewUIElement( 'span' );
+					viewRoot.insertChildren( 1, uiElement, writer );
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -430,12 +435,14 @@ describe( 'downcast-selection-converters', () => {
 					{ bold: 'true' }
 				);
 
-				const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
-				model.change( writer => {
-					writer.setSelection( modelRange );
-				} );
+				view.change( writer => {
+					const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
+					model.change( writer => {
+						writer.setSelection( modelRange );
+					} );
 
-				dispatcher.convertSelection( modelDoc.selection, [] );
+					dispatcher.convertSelection( modelDoc.selection, model.markers, writer );
+				} );
 
 				expect( viewSelection.rangeCount ).to.equal( 1 );
 
@@ -451,15 +458,17 @@ describe( 'downcast-selection-converters', () => {
 					{ bold: 'true' }
 				);
 
-				// Remove <strong></strong> manually.
-				mergeAttributes( viewSelection.getFirstPosition() );
+				view.change( writer => {
+					// Remove <strong></strong> manually.
+					writer.mergeAttributes( viewSelection.getFirstPosition() );
 
-				const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
-				model.change( writer => {
-					writer.setSelection( modelRange );
-				} );
+					const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
+					model.change( writer => {
+						writer.setSelection( modelRange );
+					} );
 
-				dispatcher.convertSelection( modelDoc.selection, [] );
+					dispatcher.convertSelection( modelDoc.selection, model.markers, writer );
+				} );
 
 				expect( viewSelection.rangeCount ).to.equal( 1 );
 
@@ -471,10 +480,11 @@ describe( 'downcast-selection-converters', () => {
 		describe( 'clearFakeSelection', () => {
 			it( 'should clear fake selection', () => {
 				dispatcher.on( 'selection', clearFakeSelection() );
-				viewSelection.setFake( true );
-
-				dispatcher.convertSelection( docSelection, [] );
 
+				view.change( writer => {
+					writer.setFakeSelection( true );
+					dispatcher.convertSelection( docSelection, model.markers, writer );
+				} );
 				expect( viewSelection.isFake ).to.be.false;
 			} );
 		} );
@@ -492,7 +502,8 @@ describe( 'downcast-selection-converters', () => {
 			model.schema.extend( '$text', { allowIn: 'td' } );
 
 			// "Universal" converter to convert table structure.
-			const tableConverter = insertElement( modelItem => new ViewContainerElement( modelItem.name ) );
+			const containerCreator = ( item, consumable, api ) => api.writer.createContainerElement( item.name );
+			const tableConverter = insertElement( containerCreator );
 			dispatcher.on( 'insert:table', tableConverter );
 			dispatcher.on( 'insert:tr', tableConverter );
 			dispatcher.on( 'insert:td', tableConverter );
@@ -512,7 +523,7 @@ describe( 'downcast-selection-converters', () => {
 						consumable.consume( selection, 'selection' );
 
 						const viewNode = conversionApi.mapper.toViewElement( node );
-						viewNode.addClass( 'selected' );
+						conversionApi.writer.addClass( 'selected', viewNode );
 					}
 				}
 			} );
@@ -581,8 +592,10 @@ describe( 'downcast-selection-converters', () => {
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 
 		// Convert model to view.
-		dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
-		dispatcher.convertSelection( docSelection, [] );
+		view.change( writer => {
+			dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+			dispatcher.convertSelection( docSelection, model.markers, writer );
+		} );
 
 		// Stringify view and check if it is same as expected.
 		expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<div>' + expectedView + '</div>' );

+ 34 - 20
packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

@@ -10,15 +10,17 @@ import ModelElement from '../../src/model/element';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
 
+import View from '../../src/view/view';
 import ViewContainerElement from '../../src/view/containerelement';
 
 describe( 'DowncastDispatcher', () => {
-	let dispatcher, doc, root, differStub, model;
+	let dispatcher, doc, root, differStub, model, view;
 
 	beforeEach( () => {
 		model = new Model();
+		view = new View();
 		doc = model.document;
-		dispatcher = new DowncastDispatcher( model );
+		dispatcher = new DowncastDispatcher();
 		root = doc.createRoot();
 
 		differStub = {
@@ -31,7 +33,7 @@ describe( 'DowncastDispatcher', () => {
 	describe( 'constructor()', () => {
 		it( 'should create DowncastDispatcher with given api', () => {
 			const apiObj = {};
-			const dispatcher = new DowncastDispatcher( model, { apiObj } );
+			const dispatcher = new DowncastDispatcher( { apiObj } );
 
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 		} );
@@ -46,7 +48,9 @@ describe( 'DowncastDispatcher', () => {
 
 			differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 			expect( dispatcher.convertInsert.calledOnce ).to.be.true;
 			expect( dispatcher.convertInsert.firstCall.args[ 0 ].isEqual( range ) ).to.be.true;
@@ -59,7 +63,9 @@ describe( 'DowncastDispatcher', () => {
 
 			differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ];
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 			expect( dispatcher.convertRemove.calledWith( position, 2, '$text' ) ).to.be.true;
 		} );
@@ -74,7 +80,9 @@ describe( 'DowncastDispatcher', () => {
 				{ type: 'attribute', position, range, attributeKey: 'key', attributeOldValue: null, attributeNewValue: 'foo' }
 			];
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 			expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true;
 		} );
@@ -94,7 +102,9 @@ describe( 'DowncastDispatcher', () => {
 				{ type: 'insert', position, length: 3 },
 			];
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 			expect( dispatcher.convertInsert.calledTwice ).to.be.true;
 			expect( dispatcher.convertRemove.calledOnce ).to.be.true;
@@ -112,7 +122,9 @@ describe( 'DowncastDispatcher', () => {
 				{ name: 'bar', range: barRange }
 			];
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
@@ -129,7 +141,9 @@ describe( 'DowncastDispatcher', () => {
 				{ name: 'bar', range: barRange }
 			];
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
@@ -250,7 +264,7 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should fire selection event', () => {
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convertSelection( doc.selection, [] );
+			dispatcher.convertSelection( doc.selection, model.markers, [] );
 
 			expect( dispatcher.fire.calledWith(
 				'selection',
@@ -270,7 +284,7 @@ describe( 'DowncastDispatcher', () => {
 				expect( consumable.test( data.selection, 'attribute:italic' ) ).to.be.null;
 			} );
 
-			dispatcher.convertSelection( doc.selection, [] );
+			dispatcher.convertSelection( doc.selection, model.markers, [] );
 		} );
 
 		it( 'should not fire attributes events for non-collapsed selection', () => {
@@ -281,7 +295,7 @@ describe( 'DowncastDispatcher', () => {
 
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convertSelection( doc.selection, [] );
+			dispatcher.convertSelection( doc.selection, model.markers, [] );
 
 			expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false;
 			expect( dispatcher.fire.calledWith( 'attribute:italic' ) ).to.be.false;
@@ -300,7 +314,7 @@ describe( 'DowncastDispatcher', () => {
 
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convertSelection( doc.selection, [] );
+			dispatcher.convertSelection( doc.selection, model.markers, [] );
 
 			expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.true;
 		} );
@@ -323,7 +337,7 @@ describe( 'DowncastDispatcher', () => {
 
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convertSelection( doc.selection, [] );
+			dispatcher.convertSelection( doc.selection, model.markers, [] );
 
 			expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false;
 		} );
@@ -339,7 +353,7 @@ describe( 'DowncastDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
-			dispatcher.convertSelection( doc.selection, markers );
+			dispatcher.convertSelection( doc.selection, model.markers, markers );
 
 			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.true;
 		} );
@@ -352,7 +366,7 @@ describe( 'DowncastDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
-			dispatcher.convertSelection( doc.selection, markers );
+			dispatcher.convertSelection( doc.selection, model.markers, markers );
 
 			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
 		} );
@@ -371,8 +385,8 @@ describe( 'DowncastDispatcher', () => {
 			const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
 
 			// Create custom highlight handler mock.
-			viewFigure.setCustomProperty( 'addHighlight', () => { } );
-			viewFigure.setCustomProperty( 'removeHighlight', () => { } );
+			viewFigure._setCustomProperty( 'addHighlight', () => { } );
+			viewFigure._setCustomProperty( 'removeHighlight', () => { } );
 
 			// Create mapper mock.
 			dispatcher.conversionApi.mapper = {
@@ -393,7 +407,7 @@ describe( 'DowncastDispatcher', () => {
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 
-			dispatcher.convertSelection( doc.selection, markers );
+			dispatcher.convertSelection( doc.selection, model.markers, markers );
 
 			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
 		} );
@@ -414,7 +428,7 @@ describe( 'DowncastDispatcher', () => {
 			} );
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
-			dispatcher.convertSelection( doc.selection, markers );
+			dispatcher.convertSelection( doc.selection, model.markers, markers );
 
 			expect( dispatcher.fire.calledWith( 'addMarker:foo' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'addMarker:bar' ) ).to.be.false;

+ 8 - 9
packages/ckeditor5-engine/tests/conversion/two-way-converters.js

@@ -21,7 +21,7 @@ import { stringify as viewStringify, parse as viewParse } from '../../src/dev-ut
 import { stringify as modelStringify } from '../../src/dev-utils/model';
 
 describe( 'two-way-converters', () => {
-	let viewDispatcher, model, schema, conversion, modelRoot, viewRoot;
+	let upcastDispatcher, model, schema, conversion, modelRoot, viewRoot;
 
 	beforeEach( () => {
 		model = new Model();
@@ -30,7 +30,7 @@ describe( 'two-way-converters', () => {
 		const modelDoc = model.document;
 		modelRoot = modelDoc.createRoot();
 
-		viewRoot = controller.view.getRoot();
+		viewRoot = controller.view.document.getRoot();
 		// Set name of view root the same as dom root.
 		// This is a mock of attaching view root to dom root.
 		viewRoot._name = 'div';
@@ -45,13 +45,13 @@ describe( 'two-way-converters', () => {
 			inheritAllFrom: '$block'
 		} );
 
-		viewDispatcher = new UpcastDispatcher( model, { schema } );
-		viewDispatcher.on( 'text', convertText() );
-		viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-		viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+		upcastDispatcher = new UpcastDispatcher( { schema } );
+		upcastDispatcher.on( 'text', convertText() );
+		upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+		upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
 		conversion = new Conversion();
-		conversion.register( 'upcast', [ viewDispatcher ] );
+		conversion.register( 'upcast', [ upcastDispatcher ] );
 		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
 	} );
 
@@ -527,9 +527,8 @@ describe( 'two-way-converters', () => {
 	function loadData( input ) {
 		const parsedView = viewParse( input );
 
-		const convertedModel = viewDispatcher.convert( parsedView );
-
 		model.change( writer => {
+			const convertedModel = upcastDispatcher.convert( parsedView, writer );
 			writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );
 			writer.insert( convertedModel, modelRoot, 0 );
 		} );

+ 19 - 20
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -27,7 +27,7 @@ import {
 import { stringify } from '../../src/dev-utils/model';
 
 describe( 'upcast-helpers', () => {
-	let dispatcher, model, schema, conversion;
+	let upcastDispatcher, model, schema, conversion;
 
 	beforeEach( () => {
 		model = new Model();
@@ -50,13 +50,13 @@ describe( 'upcast-helpers', () => {
 			allowAttributes: [ 'bold' ]
 		} );
 
-		dispatcher = new UpcastDispatcher( model, { schema } );
-		dispatcher.on( 'text', convertText() );
-		dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-		dispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+		upcastDispatcher = new UpcastDispatcher( { schema } );
+		upcastDispatcher.on( 'text', convertText() );
+		upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+		upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
 		conversion = new Conversion();
-		conversion.register( 'upcast', [ dispatcher ] );
+		conversion.register( 'upcast', [ upcastDispatcher ] );
 	} );
 
 	describe( 'upcastElementToElement', () => {
@@ -600,18 +600,18 @@ describe( 'upcast-helpers', () => {
 	} );
 
 	function expectResult( viewToConvert, modelString, marker ) {
-		const model = dispatcher.convert( viewToConvert );
+		const conversionResult = model.change( writer => upcastDispatcher.convert( viewToConvert, writer ) );
 
 		if ( marker ) {
-			expect( model.markers.has( marker.name ) ).to.be.true;
+			expect( conversionResult.markers.has( marker.name ) ).to.be.true;
 
-			const convertedMarker = model.markers.get( marker.name );
+			const convertedMarker = conversionResult.markers.get( marker.name );
 
 			expect( convertedMarker.start.path ).to.deep.equal( marker.start );
 			expect( convertedMarker.end.path ).to.deep.equal( marker.end );
 		}
 
-		expect( stringify( model ) ).to.equal( modelString );
+		expect( stringify( conversionResult ) ).to.equal( modelString );
 	}
 } );
 
@@ -627,7 +627,7 @@ describe( 'upcast-converters', () => {
 
 		context = [ '$root' ];
 
-		dispatcher = new UpcastDispatcher( model, { schema } );
+		dispatcher = new UpcastDispatcher( { schema } );
 	} );
 
 	describe( 'convertText()', () => {
@@ -636,7 +636,7 @@ describe( 'upcast-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText );
+			const conversionResult = model.change( writer => dispatcher.convert( viewText, writer ) );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -658,7 +658,7 @@ describe( 'upcast-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewText, context );
+			const conversionResult = model.change( writer => dispatcher.convert( viewText, writer, context ) );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -674,13 +674,12 @@ describe( 'upcast-converters', () => {
 
 			const viewText = new ViewText( 'foobar' );
 			dispatcher.on( 'text', convertText() );
-
-			let conversionResult = dispatcher.convert( viewText, context );
+			let conversionResult = model.change( writer => dispatcher.convert( viewText, writer, context ) );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 0 );
 
-			conversionResult = dispatcher.convert( viewText, [ '$block' ] );
+			conversionResult = model.change( writer => dispatcher.convert( viewText, writer, [ '$block' ] ) );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 1 );
@@ -693,7 +692,7 @@ describe( 'upcast-converters', () => {
 
 			dispatcher.on( 'text', convertText() );
 
-			const conversionResult = dispatcher.convert( viewText, context );
+			const conversionResult = model.change( writer => dispatcher.convert( viewText, writer, context ) );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -714,7 +713,7 @@ describe( 'upcast-converters', () => {
 			dispatcher.on( 'element', convertToModelFragment() );
 			dispatcher.on( 'documentFragment', convertToModelFragment() );
 
-			const conversionResult = dispatcher.convert( viewFragment, context );
+			const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer, context ) );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.maxOffset ).to.equal( 6 );
@@ -741,7 +740,7 @@ describe( 'upcast-converters', () => {
 				}
 			} );
 
-			const conversionResult = dispatcher.convert( viewP, context );
+			const conversionResult = model.change( writer => dispatcher.convert( viewP, writer, context ) );
 
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );
@@ -775,7 +774,7 @@ describe( 'upcast-converters', () => {
 				spy();
 			} );
 
-			dispatcher.convert( view );
+			model.change( writer => dispatcher.convert( view, writer ) );
 
 			sinon.assert.calledTwice( spy );
 		} );

+ 6 - 5
packages/ckeditor5-engine/tests/conversion/upcast-selection-converters.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import ViewDocument from '../../src/view/document';
+import View from '../../src/view/view';
 import ViewSelection from '../../src/view/selection';
 import ViewRange from '../../src/view/range';
 import createViewRoot from '../view/_utils/createroot';
@@ -17,7 +17,7 @@ import { setData as modelSetData, getData as modelGetData } from '../../src/dev-
 import { setData as viewSetData } from '../../src/dev-utils/view';
 
 describe( 'convertSelectionChange', () => {
-	let model, view, mapper, convertSelection, modelRoot, viewRoot;
+	let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot;
 
 	beforeEach( () => {
 		model = new Model();
@@ -26,8 +26,9 @@ describe( 'convertSelectionChange', () => {
 
 		modelSetData( model, '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
 
-		view = new ViewDocument();
-		viewRoot = createViewRoot( view, 'div', 'main' );
+		view = new View();
+		viewDocument = view.document;
+		viewRoot = createViewRoot( viewDocument, 'div', 'main' );
 
 		viewSetData( view, '<p>foo</p><p>bar</p>' );
 
@@ -45,7 +46,7 @@ describe( 'convertSelectionChange', () => {
 
 	it( 'should convert collapsed selection', () => {
 		const viewSelection = new ViewSelection();
-		viewSelection.setTo( ViewRange.createFromParentsAndOffsets(
+		viewSelection._setTo( ViewRange.createFromParentsAndOffsets(
 			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
 
 		convertSelection( null, { newSelection: viewSelection } );

+ 28 - 26
packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js

@@ -30,7 +30,7 @@ describe( 'UpcastDispatcher', () => {
 	describe( 'constructor()', () => {
 		it( 'should create UpcastDispatcher with passed api', () => {
 			const apiObj = {};
-			const dispatcher = new UpcastDispatcher( model, { apiObj } );
+			const dispatcher = new UpcastDispatcher( { apiObj } );
 
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 			expect( dispatcher.conversionApi ).to.have.property( 'convertItem' ).that.is.instanceof( Function );
@@ -39,7 +39,7 @@ describe( 'UpcastDispatcher', () => {
 		} );
 
 		it( 'should have properties', () => {
-			const dispatcher = new UpcastDispatcher( model );
+			const dispatcher = new UpcastDispatcher();
 
 			expect( dispatcher._removeIfEmpty ).to.instanceof( Set );
 		} );
@@ -49,7 +49,7 @@ describe( 'UpcastDispatcher', () => {
 		let dispatcher;
 
 		beforeEach( () => {
-			dispatcher = new UpcastDispatcher( model );
+			dispatcher = new UpcastDispatcher();
 		} );
 
 		it( 'should create api for current conversion process', () => {
@@ -100,7 +100,7 @@ describe( 'UpcastDispatcher', () => {
 				spy();
 			} );
 
-			dispatcher.convert( viewElement );
+			model.change( writer => dispatcher.convert( viewElement, writer ) );
 
 			// To be sure that both converters was called.
 			sinon.assert.calledTwice( spy );
@@ -115,7 +115,7 @@ describe( 'UpcastDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 
 			const viewP = new ViewContainerElement( 'p' );
-			dispatcher.convert( viewP );
+			model.change( writer => dispatcher.convert( viewP, writer ) );
 
 			expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true;
 		} );
@@ -127,9 +127,11 @@ describe( 'UpcastDispatcher', () => {
 
 			sinon.spy( dispatcher, 'fire' );
 
-			dispatcher.convert( viewText );
-			dispatcher.convert( viewElement );
-			dispatcher.convert( viewFragment );
+			model.change( writer => {
+				dispatcher.convert( viewText, writer );
+				dispatcher.convert( viewElement, writer );
+				dispatcher.convert( viewFragment, writer );
+			} );
 
 			expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true;
@@ -163,7 +165,7 @@ describe( 'UpcastDispatcher', () => {
 				data.modelRange = ModelRange.createOn( text );
 			} );
 
-			const conversionResult = dispatcher.convert( viewText );
+			const conversionResult = model.change( writer => dispatcher.convert( viewText, writer ) );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -201,7 +203,7 @@ describe( 'UpcastDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const conversionResult = dispatcher.convert( viewElement );
+			const conversionResult = model.change( writer => dispatcher.convert( viewElement, writer ) );
 
 			// Check conversion result.
 			// Result should be wrapped in document fragment.
@@ -237,7 +239,7 @@ describe( 'UpcastDispatcher', () => {
 				data.modelRange = ModelRange.createOn( text );
 			} );
 
-			const conversionResult = dispatcher.convert( viewFragment );
+			const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer ) );
 
 			// Check conversion result.
 			expect( conversionResult ).to.be.instanceof( ModelDocumentFragment );
@@ -288,7 +290,7 @@ describe( 'UpcastDispatcher', () => {
 				spy();
 			} );
 
-			const result = dispatcher.convert( viewElement );
+			const result = model.change( writer => dispatcher.convert( viewElement, writer ) );
 
 			// Empty split elements should be removed and we should have the following result:
 			// [<p></p>]<p>foo</p>
@@ -322,7 +324,7 @@ describe( 'UpcastDispatcher', () => {
 				data.modelRange = ModelRange.createIn( data.modelCursor.parent );
 			} );
 
-			const conversionResult = dispatcher.convert( viewFragment );
+			const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer ) );
 
 			expect( conversionResult.markers.size ).to.equal( 2 );
 
@@ -335,7 +337,7 @@ describe( 'UpcastDispatcher', () => {
 		} );
 
 		it( 'should convert according to given context', () => {
-			dispatcher = new UpcastDispatcher( model, { schema: model.schema } );
+			dispatcher = new UpcastDispatcher( { schema: model.schema } );
 
 			const spy = sinon.spy();
 			const viewElement = new ViewContainerElement( 'third' );
@@ -358,12 +360,12 @@ describe( 'UpcastDispatcher', () => {
 			} );
 
 			// Default context $root.
-			dispatcher.convert( viewElement );
+			model.change( writer => dispatcher.convert( viewElement, writer ) );
 			sinon.assert.calledOnce( spy );
 			expect( checkChildResult ).to.false;
 
 			// SchemaDefinition as context.
-			dispatcher.convert( viewElement, [ 'first' ] );
+			model.change( writer => dispatcher.convert( viewElement, writer, [ 'first' ] ) );
 			sinon.assert.calledTwice( spy );
 			expect( checkChildResult ).to.false;
 
@@ -374,7 +376,7 @@ describe( 'UpcastDispatcher', () => {
 				] )
 			] );
 
-			dispatcher.convert( viewElement, new ModelPosition( fragment, [ 0, 0, 0 ] ) );
+			model.change( writer => dispatcher.convert( viewElement, writer, new ModelPosition( fragment, [ 0, 0, 0 ] ) ) );
 			sinon.assert.calledThrice( spy );
 			expect( checkChildResult ).to.true;
 		} );
@@ -397,7 +399,7 @@ describe( 'UpcastDispatcher', () => {
 			// Put nodes to documentFragment, this will mock root element and makes possible to create range on them.
 			rootMock = new ModelDocumentFragment( [ modelP, modelText ] );
 
-			dispatcher = new UpcastDispatcher( model, { schema: model.schema } );
+			dispatcher = new UpcastDispatcher( { schema: model.schema } );
 
 			dispatcher.on( 'element:p', ( evt, data ) => {
 				spyP();
@@ -456,7 +458,7 @@ describe( 'UpcastDispatcher', () => {
 					expect( textResult.modelCursor.path ).to.deep.equal( [ 7 ] );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -471,7 +473,7 @@ describe( 'UpcastDispatcher', () => {
 					expect( conversionApi.convertItem( viewNull, data.modelCursor ).modelRange ).to.equal( null );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyNull.calledOnce ).to.be.true;
@@ -485,7 +487,7 @@ describe( 'UpcastDispatcher', () => {
 				} );
 
 				expect( () => {
-					dispatcher.convert( new ViewDocumentFragment() );
+					model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) );
 				} ).to.throw( CKEditorError, /^view-conversion-dispatcher-incorrect-result/ );
 
 				expect( spy.calledOnce ).to.be.true;
@@ -513,7 +515,7 @@ describe( 'UpcastDispatcher', () => {
 					expect( result.modelCursor.path ).to.deep.equal( [ 7 ] );
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ) );
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), writer ) );
 
 				expect( spy.calledOnce ).to.be.true;
 				expect( spyP.calledOnce ).to.be.true;
@@ -546,7 +548,7 @@ describe( 'UpcastDispatcher', () => {
 					spy();
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) );
 				sinon.assert.calledOnce( spy );
 			} );
 
@@ -583,7 +585,7 @@ describe( 'UpcastDispatcher', () => {
 					spy();
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) );
 				sinon.assert.calledOnce( spy );
 			} );
 
@@ -603,7 +605,7 @@ describe( 'UpcastDispatcher', () => {
 					spy();
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment() );
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) );
 				sinon.assert.calledOnce( spy );
 			} );
 
@@ -622,7 +624,7 @@ describe( 'UpcastDispatcher', () => {
 					spy();
 				} );
 
-				dispatcher.convert( new ViewDocumentFragment(), [ '$root', 'paragraph' ] );
+				model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer, [ '$root', 'paragraph' ] ) );
 				sinon.assert.calledOnce( spy );
 			} );
 		} );

+ 4 - 4
packages/ckeditor5-engine/tests/conversion/viewconsumable.js

@@ -474,8 +474,8 @@ describe( 'ViewConsumable', () => {
 		} );
 
 		it( 'should add all attributes', () => {
-			el.setAttribute( 'title', 'foobar' );
-			el.setAttribute( 'href', 'https://ckeditor.com' );
+			el._setAttribute( 'title', 'foobar' );
+			el._setAttribute( 'href', 'https://ckeditor.com' );
 
 			const consumables = ViewConsumable.consumablesFromElement( el );
 			expect( consumables.attribute.length ).to.equal( 2 );
@@ -487,7 +487,7 @@ describe( 'ViewConsumable', () => {
 		} );
 
 		it( 'should add all classes', () => {
-			el.addClass( 'foo', 'bar', 'baz' );
+			el._addClass( [ 'foo', 'bar', 'baz' ] );
 
 			const consumables = ViewConsumable.consumablesFromElement( el );
 			expect( consumables.class.length ).to.equal( 3 );
@@ -500,7 +500,7 @@ describe( 'ViewConsumable', () => {
 		} );
 
 		it( 'should add all styles', () => {
-			el.setStyle( {
+			el._setStyle( {
 				color: 'red',
 				position: 'absolute'
 			} );

+ 2 - 1
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -788,7 +788,8 @@ describe( 'debug tools', () => {
 			const model = editor.model;
 			const modelDoc = model.document;
 			const modelRoot = modelDoc.getRoot();
-			const viewDoc = editor.editing.view;
+			const view = editor.editing.view;
+			const viewDoc = view.document;
 
 			model.change( () => {
 				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );

+ 28 - 22
packages/ckeditor5-engine/tests/dev-utils/view.js

@@ -16,7 +16,7 @@ import UIElement from '../../src/view/uielement';
 import Text from '../../src/view/text';
 import Selection from '../../src/view/selection';
 import Range from '../../src/view/range';
-import Document from '../../src/view/document';
+import View from '../../src/view/view';
 import XmlDataProcessor from '../../src/dataprocessor/xmldataprocessor';
 import createViewRoot from '../view/_utils/createroot';
 
@@ -36,12 +36,13 @@ describe( 'view test utils', () => {
 			it( 'should use stringify method', () => {
 				const element = document.createElement( 'div' );
 				const stringifySpy = sandbox.spy( getData, '_stringify' );
-				const viewDocument = new Document();
+				const view = new View();
+				const viewDocument = view.document;
 				const options = { showType: false, showPriority: false, withoutSelection: true };
 				const root = createAttachedRoot( viewDocument, element );
 				root.appendChildren( new Element( 'p' ) );
 
-				expect( getData( viewDocument, options ) ).to.equal( '<p></p>' );
+				expect( getData( view, options ) ).to.equal( '<p></p>' );
 				sinon.assert.calledOnce( stringifySpy );
 				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
 				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( null );
@@ -50,20 +51,23 @@ describe( 'view test utils', () => {
 				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 
 			it( 'should use stringify method with selection', () => {
 				const element = document.createElement( 'div' );
 				const stringifySpy = sandbox.spy( getData, '_stringify' );
-				const viewDocument = new Document();
+				const view = new View();
+				const viewDocument = view.document;
 				const options = { showType: false, showPriority: false };
 				const root = createAttachedRoot( viewDocument, element );
 				root.appendChildren( new Element( 'p' ) );
 
-				viewDocument.selection.setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+				view.change( writer => {
+					writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+				} );
 
-				expect( getData( viewDocument, options ) ).to.equal( '[<p></p>]' );
+				expect( getData( view, options ) ).to.equal( '[<p></p>]' );
 				sinon.assert.calledOnce( stringifySpy );
 				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
 				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( viewDocument.selection );
@@ -72,56 +76,58 @@ describe( 'view test utils', () => {
 				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 
 			it( 'should throw an error when passing invalid document', () => {
 				expect( () => {
-					getData( { invalid: 'document' } );
-				} ).to.throw( TypeError, 'Document needs to be an instance of module:engine/view/document~Document.' );
+					getData( { invalid: 'view' } );
+				} ).to.throw( TypeError, 'View needs to be an instance of module:engine/view/view~View.' );
 			} );
 		} );
 
 		describe( 'setData', () => {
 			it( 'should use parse method', () => {
-				const viewDocument = new Document();
+				const view = new View();
+				const viewDocument = view.document;
 				const data = 'foobar<b>baz</b>';
 				const parseSpy = sandbox.spy( setData, '_parse' );
 
 				createAttachedRoot( viewDocument, document.createElement( 'div' ) );
-				setData( viewDocument, data );
+				setData( view, data );
 
-				expect( getData( viewDocument ) ).to.equal( 'foobar<b>baz</b>' );
+				expect( getData( view ) ).to.equal( 'foobar<b>baz</b>' );
 				sinon.assert.calledOnce( parseSpy );
 				const args = parseSpy.firstCall.args;
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 
 			it( 'should use parse method with selection', () => {
-				const viewDocument = new Document();
+				const view = new View();
+				const viewDocument = view.document;
 				const data = '[<b>baz</b>]';
 				const parseSpy = sandbox.spy( setData, '_parse' );
 
 				createAttachedRoot( viewDocument, document.createElement( 'div' ) );
-				setData( viewDocument, data );
+				setData( view, data );
 
-				expect( getData( viewDocument ) ).to.equal( '[<b>baz</b>]' );
+				expect( getData( view ) ).to.equal( '[<b>baz</b>]' );
 				const args = parseSpy.firstCall.args;
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 
 			it( 'should throw an error when passing invalid document', () => {
 				expect( () => {
-					setData( { invalid: 'document' } );
-				} ).to.throw( TypeError, 'Document needs to be an instance of module:engine/view/document~Document.' );
+					setData( { invalid: 'view' } );
+				} ).to.throw( TypeError, 'View needs to be an instance of module:engine/view/view~View.' );
 			} );
 		} );
 	} );
@@ -449,10 +455,10 @@ describe( 'view test utils', () => {
 		it( 'should parse element priority', () => {
 			const parsed1 = parse( '<b view-priority="12"></b>' );
 			const attribute1 = new AttributeElement( 'b' );
-			attribute1.priority = 12;
+			attribute1._priority = 12;
 			const parsed2 = parse( '<attribute:b view-priority="44"></attribute:b>' );
 			const attribute2 = new AttributeElement( 'b' );
-			attribute2.priority = 44;
+			attribute2._priority = 44;
 
 			parsed1.isSimilar( attribute1 );
 			expect( parsed1.isSimilar( attribute1 ) ).to.be.true;

+ 16 - 14
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -6,8 +6,7 @@
 /* global console, window, document */
 
 import ModelRange from '../../src/model/range';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewText from '../../src/view/text';
+import ViewPosition from '../../src/view/position';
 
 import {
 	upcastElementToElement,
@@ -40,7 +39,7 @@ class FancyWidget extends Plugin {
 	init() {
 		const editor = this.editor;
 		const schema = editor.model.schema;
-		const data = editor.data;
+		const conversion = editor.conversion;
 
 		// Configure schema.
 		schema.register( 'fancywidget', {
@@ -48,19 +47,22 @@ class FancyWidget extends Plugin {
 		} );
 		schema.extend( 'fancywidget', { allowIn: '$root' } );
 
-		downcastElementToElement( {
+		conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
 			model: 'fancywidget',
-			view: () => {
-				const widgetElement = new ViewContainerElement( 'figure', { class: 'fancy-widget' }, new ViewText( 'widget' ) );
+			view: ( modelItem, consumable, conversionApi ) => {
+				const viewWriter = conversionApi.writer;
+				const widgetElement = viewWriter.createContainerElement( 'figure', { class: 'fancy-widget' } );
+				viewWriter.insert( ViewPosition.createAt( widgetElement ), viewWriter.createText( 'widget' ) );
 
-				return toWidget( widgetElement );
+				return toWidget( widgetElement, viewWriter );
 			}
-		} )( data.downcastDispatcher );
+		} ) );
 
-		upcastElementToElement( {
-			view: 'figure',
-			model: 'fancywidget'
-		} )( data.upcastDispatcher );
+		conversion.for( 'upcast' )
+			.add( upcastElementToElement( {
+				view: 'figure',
+				model: 'fancywidget'
+			} ) );
 	}
 }
 
@@ -71,12 +73,12 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), {
 	.then( editor => {
 		window.editor = editor;
 
-		downcastMarkerToHighlight( {
+		editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( {
 			model: 'marker',
 			view: data => ( {
 				class: 'highlight-' + data.markerName.split( ':' )[ 1 ]
 			} )
-		} );
+		} ) );
 
 		document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => {
 			addMarker( editor, 'yellow' );

+ 2 - 2
packages/ckeditor5-engine/tests/manual/markers.js

@@ -35,7 +35,7 @@ ClassicEditor
 		window.editor = editor;
 		model = editor.model;
 
-		downcastMarkerToHighlight( {
+		editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( {
 			model: 'highlight',
 			view: data => {
 				const color = data.markerName.split( ':' )[ 1 ];
@@ -45,7 +45,7 @@ ClassicEditor
 					priority: 1
 				};
 			}
-		} );
+		} ) );
 
 		window.document.getElementById( 'add-yellow' ).addEventListener( 'mousedown', e => {
 			e.preventDefault();

+ 5 - 7
packages/ckeditor5-engine/tests/manual/nestededitable.js

@@ -13,7 +13,6 @@ import {
 	downcastElementToElement
 } from '../../src/conversion/downcast-converters';
 
-import ViewEditableElement from '../../src/view/editableelement';
 import { getData } from '../../src/dev-utils/model';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
@@ -30,7 +29,6 @@ class NestedEditable extends Plugin {
 	init() {
 		const editor = this.editor;
 		const editing = editor.editing;
-		const viewDocument = editing.view;
 		const schema = editor.model.schema;
 
 		schema.register( 'figure', {
@@ -60,15 +58,15 @@ class NestedEditable extends Plugin {
 
 		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
 			model: 'figcaption',
-			view: () => {
-				const element = new ViewEditableElement( 'figcaption', { contenteditable: 'true' } );
-				element.document = viewDocument;
+			view: ( modelItem, consumable, conversionApi ) => {
+				const viewWriter = conversionApi.writer;
+				const element = viewWriter.createEditableElement( 'figcaption', { contenteditable: 'true' } );
 
 				element.on( 'change:isFocused', ( evt, property, is ) => {
 					if ( is ) {
-						element.addClass( 'focused' );
+						editing.view.change( writer => writer.addClass( 'focused', element ) );
 					} else {
-						element.removeClass( 'focused' );
+						editing.view.change( writer => writer.removeClass( 'focused', element ) );
 					}
 				} );
 

+ 5 - 4
packages/ckeditor5-engine/tests/manual/placeholder.js

@@ -20,13 +20,14 @@ ClassicEditor
 		toolbar: [ 'headings', '|', 'undo', 'redo' ]
 	} )
 	.then( editor => {
-		const viewDoc = editor.editing.view;
+		const view = editor.editing.view;
+		const viewDoc = view.document;
 		const header = viewDoc.getRoot().getChild( 0 );
 		const paragraph = viewDoc.getRoot().getChild( 1 );
 
-		attachPlaceholder( header, 'Type some header text...' );
-		attachPlaceholder( paragraph, 'Type some paragraph text...' );
-		viewDoc.render();
+		attachPlaceholder( view, header, 'Type some header text...' );
+		attachPlaceholder( view, paragraph, 'Type some paragraph text...' );
+		view.render();
 	} )
 	.catch( err => {
 		console.error( err.stack );

+ 1 - 1
packages/ckeditor5-engine/tests/manual/tickets/880/1.js

@@ -18,7 +18,7 @@ ClassicEditor
 	.then( editor => {
 		window.editor = editor;
 
-		editor.editing.view.on( 'selectionChange', () => {
+		editor.editing.view.document.on( 'selectionChange', () => {
 			editor.model.change( () => {
 			} );
 			console.log( 'selectionChange', ( new Date() ).getTime() );

+ 9 - 8
packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js

@@ -12,11 +12,9 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 
-import AttributeContainer from '../../../../src/view/attributeelement';
-import ViewContainer from '../../../../src/view/containerelement';
+import ViewPosition from '../../../../src/view/position';
 import { downcastElementToElement } from '../../../../src/conversion/downcast-converters';
 import { setData } from '../../../../src/dev-utils/model';
-import ViewEditable from '../../../../src/view/editableelement';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
@@ -45,16 +43,19 @@ ClassicEditor
 		editor.conversion.for( 'downcast' )
 			.add( downcastElementToElement( {
 				model: 'widget',
-				view: () => {
-					const b = new AttributeContainer( 'b' );
-					const div = new ViewContainer( 'div', null, b );
+				view: ( modelItem, consumable, conversionApi ) => {
+					const writer = conversionApi.writer;
+					const b = writer.createAttributeElement( 'b' );
+					const div = writer.createContainerElement( 'div' );
 
-					return toWidget( div, { label: 'element label' } );
+					writer.insert( ViewPosition.createAt( div ), b );
+
+					return toWidget( div, writer, { label: 'element label' } );
 				}
 			} ) )
 			.add( downcastElementToElement( {
 				model: 'nested',
-				view: () => new ViewEditable( 'figcaption', { contenteditable: true } )
+				view: ( item, consumable, api ) => api.writer.createEditableElement( 'figcaption', { contenteditable: true } )
 			} ) );
 
 		setData( editor.model,

+ 394 - 0
packages/ckeditor5-engine/tests/model/utils/modifyselection.js

@@ -411,6 +411,400 @@ describe( 'DataController utils', () => {
 			} );
 		} );
 
+		describe( 'unit=word', () => {
+			describe( 'within element', () => {
+				test(
+					'does nothing on empty content',
+					'[]',
+					'[]',
+					{ unit: 'word' }
+				);
+
+				test(
+					'does nothing on empty content (with empty element)',
+					'<p>[]</p>',
+					'<p>[]</p>'
+				);
+
+				test(
+					'does nothing on empty content (backward)',
+					'[]',
+					'[]',
+					{ unit: 'word', direction: 'backward' }
+				);
+
+				test(
+					'does nothing on root boundary',
+					'<p>foo[]</p>',
+					'<p>foo[]</p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'does nothing on root boundary (backward)',
+					'<p>[]foo</p>',
+					'<p>[]foo</p>',
+					{ unit: 'word', direction: 'backward' }
+				);
+
+				for ( const char of ' ,.?!:;"-()'.split( '' ) ) {
+					testStopCharacter( char );
+				}
+
+				test(
+					'extends whole word forward (non-collapsed)',
+					'<p>f[o]obar</p>',
+					'<p>f[oobar]</p>',
+					{ unit: 'word' }
+				);
+
+				it( 'extends whole word backward (non-collapsed)', () => {
+					setData( model, '<p>foo ba[a]r</p>', { lastRangeBackward: true } );
+
+					modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo [baa]r</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				test(
+					'extends to element boundary',
+					'<p>fo[]oo</p>',
+					'<p>fo[oo]</p>',
+					{ unit: 'word' }
+				);
+
+				it( 'extends to element boundary (backward)', () => {
+					setData( model, '<p>ff[]oo</p>' );
+
+					modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[ff]oo</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				test(
+					'expands forward selection to the word start',
+					'<p>foo bar[b]az</p>',
+					'<p>foo [bar]baz</p>',
+					{ unit: 'word', direction: 'backward' }
+				);
+
+				it( 'expands backward selection to the word end', () => {
+					setData( model, '<p>foo[b]ar baz</p>', { lastRangeBackward: true } );
+
+					modifySelection( model, doc.selection, { unit: 'word' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foob[ar] baz</p>' );
+					expect( doc.selection.isBackward ).to.false;
+				} );
+
+				test(
+					'unicode support - combining mark forward',
+					'<p>foo[]b̂ar</p>',
+					'<p>foo[b̂ar]</p>',
+					{ unit: 'word' }
+				);
+
+				it( 'unicode support - combining mark backward', () => {
+					setData( model, '<p>foob̂[]ar</p>' );
+
+					modifySelection( model, doc.selection, { direction: 'backward', unit: 'word' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[foob̂]ar</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				test(
+					'unicode support - combining mark multiple',
+					'<p>fo[]o̻̐ͩbar</p>',
+					'<p>fo[o̻̐ͩbar]</p>',
+					{ unit: 'word' }
+				);
+
+				it( 'unicode support - combining mark multiple backward', () => {
+					setData( model, '<p>foo̻̐ͩ[]bar</p>' );
+
+					modifySelection( model, doc.selection, { direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				test(
+					'unicode support - combining mark to the end',
+					'<p>f[o]o̻̐ͩ</p>',
+					'<p>f[oo̻̐ͩ]</p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'unicode support - surrogate pairs forward',
+					'<p>[]foo\uD83D\uDCA9</p>',
+					'<p>[foo\uD83D\uDCA9]</p>',
+					{ unit: 'word' }
+				);
+
+				it( 'unicode support - surrogate pairs backward', () => {
+					setData( model, '<p>foo\uD83D\uDCA9[]</p>' );
+
+					modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[foo\uD83D\uDCA9]</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				function testStopCharacter( stopCharacter ) {
+					describe( `stop character: "${ stopCharacter }"`, () => {
+						test(
+							'extends whole word forward',
+							`<p>f[]oo${ stopCharacter }bar</p>`,
+							`<p>f[oo]${ stopCharacter }bar</p>`,
+							{ unit: 'word' }
+						);
+
+						it( 'extends whole word backward to the previous word', () => {
+							setData( model, `<p>foo${ stopCharacter }ba[]r</p>`, { lastRangeBackward: true } );
+
+							modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+							expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `<p>foo${ stopCharacter }[ba]r</p>` );
+							expect( doc.selection.isBackward ).to.true;
+						} );
+
+						it( 'extends whole word backward', () => {
+							setData( model, `<p>fo[]o${ stopCharacter }bar</p>`, { lastRangeBackward: true } );
+
+							modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+							expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `<p>[fo]o${ stopCharacter }bar</p>` );
+							expect( doc.selection.isBackward ).to.true;
+						} );
+
+						test(
+							'ignores attributes when in one word - case 1',
+							`<p>foo[]<$text bold="true">bar</$text>baz${ stopCharacter }foobarbaz</p>`,
+							`<p>foo[<$text bold="true">bar</$text>baz]${ stopCharacter }foobarbaz</p>`,
+							{ unit: 'word' }
+						);
+
+						test(
+							'ignores attributes when in one word - case 2',
+							`<p>foo[]<$text bold="true">bar</$text>${ stopCharacter }foobarbaz</p>`,
+							`<p>foo[<$text bold="true">bar</$text>]${ stopCharacter }foobarbaz</p>`,
+							{ unit: 'word' }
+						);
+
+						test(
+							'ignores attributes when in one word - case 3',
+							`<p>foo[]<$text bold="true">bar</$text><$text italic="true">baz</$text>baz${ stopCharacter }foobarbaz</p>`,
+							`<p>foo[<$text bold="true">bar</$text><$text italic="true">baz</$text>baz]${ stopCharacter }foobarbaz</p>`,
+							{ unit: 'word' }
+						);
+
+						it( 'extends whole word backward to the previous word ignoring attributes - case 1', () => {
+							setData(
+								model,
+								`<p>foobarbaz${ stopCharacter }foo<$text bold="true">bar</$text>baz[]</p>`
+							);
+
+							modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+							expect( stringify( doc.getRoot(), doc.selection ) ).to.equal(
+								`<p>foobarbaz${ stopCharacter }[foo<$text bold="true">bar</$text>baz]</p>`
+							);
+							expect( doc.selection.isBackward ).to.true;
+						} );
+
+						it( 'extends whole word backward to the previous word ignoring attributes - case 2', () => {
+							setData(
+								model,
+								`<p>foobarbaz${ stopCharacter }<$text bold="true">bar</$text>baz[]</p>`
+							);
+
+							modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+							expect( stringify( doc.getRoot(), doc.selection ) ).to.equal(
+								`<p>foobarbaz${ stopCharacter }[<$text bold="true">bar</$text>baz]</p>`
+							);
+							expect( doc.selection.isBackward ).to.true;
+						} );
+					} );
+				}
+			} );
+
+			describe( 'beyond element', () => {
+				test(
+					'extends over boundary of empty elements',
+					'<p>[]</p><p></p><p></p>',
+					'<p>[</p><p>]</p><p></p>',
+					{ unit: 'word' }
+				);
+
+				it( 'extends over boundary of empty elements (backward)', () => {
+					setData( model, '<p></p><p></p><p>[]</p>' );
+
+					modifySelection( model, doc.selection, { direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				test(
+					'extends over boundary of non-empty elements',
+					'<p>a[]</p><p>bcd</p>',
+					'<p>a[</p><p>]bcd</p>',
+					{ unit: 'word' }
+				);
+
+				it( 'extends over boundary of non-empty elements (backward)', () => {
+					setData( model, '<p>a</p><p>[]bcd</p>' );
+
+					modifySelection( model, doc.selection, { direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				test(
+					'extends over character after boundary',
+					'<p>a[</p><p>]bcd</p>',
+					'<p>a[</p><p>bcd]</p>',
+					{ unit: 'word' }
+				);
+
+				it( 'extends over character after boundary (backward)', () => {
+					setData( model, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
+
+					modifySelection( model, doc.selection, { direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
+					expect( doc.selection.isBackward ).to.true;
+				} );
+
+				test(
+					'stops on the first position where text is allowed - inside block',
+					'<p>a[]</p><p><x>bcd</x></p>',
+					'<p>a[</p><p>]<x>bcd</x></p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'stops on the first position where text is allowed - inside inline element',
+					'<p>a[</p><p>]<x>bcd</x>ef</p>',
+					'<p>a[</p><p><x>]bcd</x>ef</p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'extends over element when next node is a text',
+					'<p><x>a[]</x>bc</p>',
+					'<p><x>a[</x>]bc</p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'extends over element when next node is a text - backward',
+					'<p>ab<x>[]c</x></p>',
+					'<p>ab[<x>]c</x></p>',
+					{ unit: 'word', direction: 'backward' }
+				);
+
+				it( 'shrinks over boundary of empty elements', () => {
+					setData( model, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
+
+					modifySelection( model, doc.selection, { unit: 'word' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[]</p>' );
+					expect( doc.selection.isBackward ).to.false;
+				} );
+
+				it( 'shrinks over boundary of empty elements (backward)', () => {
+					setData( model, '<p>[</p><p>]</p>' );
+
+					modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[]</p><p></p>' );
+					expect( doc.selection.isBackward ).to.false;
+				} );
+
+				it( 'shrinks over boundary of non-empty elements', () => {
+					setData( model, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
+
+					modifySelection( model, doc.selection, { unit: 'word' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
+					expect( doc.selection.isBackward ).to.false;
+				} );
+
+				test(
+					'shrinks over boundary of non-empty elements (backward)',
+					'<p>a[</p><p>]b</p>',
+					'<p>a[]</p><p>b</p>',
+					{ unit: 'word', direction: 'backward' }
+				);
+
+				it( 'updates selection attributes', () => {
+					setData( model, '<p><$text bold="true">foo</$text>[b]</p>' );
+
+					modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
+
+					expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
+					expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
+				} );
+			} );
+
+			describe( 'beyond element – skipping incorrect positions', () => {
+				beforeEach( () => {
+					model.schema.register( 'quote' );
+					model.schema.extend( 'quote', { allowIn: '$root' } );
+					model.schema.extend( '$block', { allowIn: 'quote' } );
+				} );
+
+				test(
+					'skips position at the beginning of an element which does not allow text',
+					'<p>x[]</p><quote><p>y</p></quote><p>z</p>',
+					'<p>x[</p><quote><p>]y</p></quote><p>z</p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'skips position at the end of an element which does not allow text - backward',
+					'<p>x</p><quote><p>y</p></quote><p>[]z</p>',
+					'<p>x</p><quote><p>y[</p></quote><p>]z</p>',
+					{ unit: 'word', direction: 'backward' }
+				);
+
+				test(
+					'skips position at the end of an element which does not allow text',
+					'<p>x[</p><quote><p>y]</p></quote><p>z</p>',
+					'<p>x[</p><quote><p>y</p></quote><p>]z</p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'skips position at the beginning of an element which does not allow text - backward',
+					'<p>x</p><quote><p>[]y</p></quote><p>z</p>',
+					'<p>x[</p><quote><p>]y</p></quote><p>z</p>',
+					{ unit: 'word', direction: 'backward' }
+				);
+
+				test(
+					'extends to an empty block after skipping incorrect position',
+					'<p>x[]</p><quote><p></p></quote><p>z</p>',
+					'<p>x[</p><quote><p>]</p></quote><p>z</p>',
+					{ unit: 'word' }
+				);
+
+				test(
+					'extends to an empty block after skipping incorrect position - backward',
+					'<p>x</p><quote><p></p></quote><p>[]z</p>',
+					'<p>x</p><quote><p>[</p></quote><p>]z</p>',
+					{ unit: 'word', direction: 'backward' }
+				);
+			} );
+		} );
+
 		describe( 'objects handling', () => {
 			beforeEach( () => {
 				model.schema.register( 'obj', {

+ 1 - 1
packages/ckeditor5-engine/tests/view/_utils/createroot.js

@@ -16,7 +16,7 @@ import RootEditableElement from '../../../src/view/rooteditableelement';
 export default function createRoot( doc, name = 'div', rootName = 'main' ) {
 	const root = new RootEditableElement( name );
 
-	root.document = doc;
+	root._document = doc;
 	root.rootName = rootName;
 	doc.roots.add( root );
 

+ 4 - 4
packages/ckeditor5-engine/tests/view/attributeelement.js

@@ -51,7 +51,7 @@ describe( 'AttributeElement', () => {
 	describe( 'clone', () => {
 		it( 'should clone element with priority', () => {
 			const el = new AttributeElement( 'b' );
-			el.priority = 7;
+			el._priority = 7;
 
 			const clone = el.clone();
 
@@ -64,17 +64,17 @@ describe( 'AttributeElement', () => {
 	describe( 'isSimilar', () => {
 		it( 'should return true if priorities are the same', () => {
 			const b1 = new AttributeElement( 'b' );
-			b1.priority = 7;
+			b1._priority = 7;
 
 			const b2 = new AttributeElement( 'b' );
-			b2.priority = 7;
+			b2._priority = 7;
 
 			expect( b1.isSimilar( b2 ) ).to.be.true;
 		} );
 
 		it( 'should return false if priorities are different', () => {
 			const b1 = new AttributeElement( 'b' );
-			b1.priority = 7;
+			b1._priority = 7;
 
 			const b2 = new AttributeElement( 'b' ); // default priority
 

+ 61 - 0
packages/ckeditor5-engine/tests/view/document.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+import Document from '../../src/view/document';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import count from '@ckeditor/ckeditor5-utils/src/count';
+import createViewRoot from './_utils/createroot';
+
+testUtils.createSinonSandbox();
+
+describe( 'Document', () => {
+	let domRoot, viewDocument;
+
+	beforeEach( () => {
+		domRoot = createElement( document, 'div', {
+			id: 'editor',
+			contenteditable: 'true'
+		} );
+		document.body.appendChild( domRoot );
+
+		viewDocument = new Document();
+	} );
+
+	afterEach( () => {
+		domRoot.remove();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should create Document with all properties', () => {
+			expect( count( viewDocument.roots ) ).to.equal( 0 );
+			expect( viewDocument ).to.have.property( 'isReadOnly' ).to.false;
+		} );
+	} );
+
+	describe( 'getRoot()', () => {
+		it( 'should return "main" root', () => {
+			createViewRoot( viewDocument, 'div', 'main' );
+
+			expect( count( viewDocument.roots ) ).to.equal( 1 );
+
+			expect( viewDocument.getRoot() ).to.equal( viewDocument.roots.get( 'main' ) );
+		} );
+
+		it( 'should return named root', () => {
+			createViewRoot( viewDocument, 'h1', 'header' );
+
+			expect( count( viewDocument.roots ) ).to.equal( 1 );
+
+			expect( viewDocument.getRoot( 'header' ) ).to.equal( viewDocument.roots.get( 'header' ) );
+		} );
+
+		it( 'should return null when trying to get non-existent root', () => {
+			expect( viewDocument.getRoot( 'not-existing' ) ).to.null;
+		} );
+	} );
+} );

+ 0 - 414
packages/ckeditor5-engine/tests/view/document/document.js

@@ -1,414 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals document */
-
-import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-import Document from '../../../src/view/document';
-import Observer from '../../../src/view/observer/observer';
-import MutationObserver from '../../../src/view/observer/mutationobserver';
-import SelectionObserver from '../../../src/view/observer/selectionobserver';
-import FocusObserver from '../../../src/view/observer/focusobserver';
-import KeyObserver from '../../../src/view/observer/keyobserver';
-import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
-import Renderer from '../../../src/view/renderer';
-import ViewRange from '../../../src/view/range';
-import DomConverter from '../../../src/view/domconverter';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import count from '@ckeditor/ckeditor5-utils/src/count';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-import global from '@ckeditor/ckeditor5-utils/src/dom/global';
-import createViewRoot from '../_utils/createroot';
-
-testUtils.createSinonSandbox();
-
-describe( 'Document', () => {
-	const DEFAULT_OBSERVERS_COUNT = 5;
-	let ObserverMock, ObserverMockGlobalCount, instantiated, enabled, domRoot, viewDocument;
-
-	beforeEach( () => {
-		domRoot = createElement( document, 'div', {
-			id: 'editor',
-			contenteditable: 'true'
-		} );
-		document.body.appendChild( domRoot );
-
-		instantiated = 0;
-		enabled = 0;
-
-		ObserverMock = class extends Observer {
-			constructor( viewDocument ) {
-				super( viewDocument );
-
-				this.enable = sinon.spy();
-				this.disable = sinon.spy();
-				this.observe = sinon.spy();
-			}
-		};
-
-		ObserverMockGlobalCount = class extends Observer {
-			constructor( viewDocument ) {
-				super( viewDocument );
-				instantiated++;
-
-				this.observe = sinon.spy();
-			}
-
-			enable() {
-				enabled++;
-			}
-		};
-
-		viewDocument = new Document();
-	} );
-
-	afterEach( () => {
-		viewDocument.destroy();
-		domRoot.remove();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create Document with all properties', () => {
-			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-			expect( count( viewDocument.roots ) ).to.equal( 0 );
-			expect( viewDocument ).to.have.property( 'renderer' ).to.instanceOf( Renderer );
-			expect( viewDocument ).to.have.property( 'domConverter' ).to.instanceOf( DomConverter );
-			expect( viewDocument ).to.have.property( 'isReadOnly' ).to.false;
-			expect( viewDocument ).to.have.property( 'isFocused' ).to.false;
-		} );
-
-		it( 'should add default observers', () => {
-			expect( count( viewDocument._observers ) ).to.equal( DEFAULT_OBSERVERS_COUNT );
-			expect( viewDocument.getObserver( MutationObserver ) ).to.be.instanceof( MutationObserver );
-			expect( viewDocument.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
-			expect( viewDocument.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
-			expect( viewDocument.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
-			expect( viewDocument.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
-		} );
-	} );
-
-	describe( 'attachDomRoot()', () => {
-		it( 'should attach DOM element to main view element', () => {
-			const domDiv = document.createElement( 'div' );
-			const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-
-			viewDocument.attachDomRoot( domDiv );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-
-			expect( viewDocument.getDomRoot() ).to.equal( domDiv );
-			expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
-
-			expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
-		} );
-
-		it( 'should attach DOM element to custom view element', () => {
-			const domH1 = document.createElement( 'h1' );
-			const viewH1 = createViewRoot( viewDocument, 'h1', 'header' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-
-			viewDocument.attachDomRoot( domH1, 'header' );
-
-			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-			expect( viewDocument.getDomRoot( 'header' ) ).to.equal( domH1 );
-			expect( viewDocument.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
-			expect( viewDocument.renderer.markedChildren.has( viewH1 ) ).to.be.true;
-		} );
-
-		it( 'should call observe on each observer', () => {
-			// The variable will be overwritten.
-			viewDocument.destroy();
-
-			viewDocument = new Document( document.createElement( 'div' ) );
-			viewDocument.renderer.render = sinon.spy();
-
-			const domDiv1 = document.createElement( 'div' );
-			domDiv1.setAttribute( 'id', 'editor' );
-
-			const domDiv2 = document.createElement( 'div' );
-			domDiv2.setAttribute( 'id', 'editor' );
-
-			const observerMock = viewDocument.addObserver( ObserverMock );
-			const observerMockGlobalCount = viewDocument.addObserver( ObserverMockGlobalCount );
-
-			createViewRoot( viewDocument, 'div', 'root1' );
-			viewDocument.attachDomRoot( document.createElement( 'div' ), 'root1' );
-
-			sinon.assert.calledOnce( observerMock.observe );
-			sinon.assert.calledOnce( observerMockGlobalCount.observe );
-		} );
-	} );
-
-	describe( 'getRoot()', () => {
-		it( 'should return "main" root', () => {
-			createViewRoot( viewDocument, 'div', 'main' );
-
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
-
-			expect( viewDocument.getRoot() ).to.equal( viewDocument.roots.get( 'main' ) );
-		} );
-
-		it( 'should return named root', () => {
-			createViewRoot( viewDocument, 'h1', 'header' );
-
-			expect( count( viewDocument.roots ) ).to.equal( 1 );
-
-			expect( viewDocument.getRoot( 'header' ) ).to.equal( viewDocument.roots.get( 'header' ) );
-		} );
-
-		it( 'should return null when trying to get non-existent root', () => {
-			expect( viewDocument.getRoot( 'not-existing' ) ).to.null;
-		} );
-	} );
-
-	describe( 'addObserver()', () => {
-		beforeEach( () => {
-			// The variable will be overwritten.
-			viewDocument.destroy();
-
-			viewDocument = new Document( document.createElement( 'div' ) );
-			viewDocument.renderer.render = sinon.spy();
-		} );
-
-		afterEach( () => {
-			viewDocument.destroy();
-		} );
-
-		it( 'should be instantiated and enabled on adding', () => {
-			const observerMock = viewDocument.addObserver( ObserverMock );
-
-			expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
-
-			expect( observerMock ).to.have.property( 'document', viewDocument );
-			sinon.assert.calledOnce( observerMock.enable );
-		} );
-
-		it( 'should return observer instance each time addObserver is called', () => {
-			const observerMock1 = viewDocument.addObserver( ObserverMock );
-			const observerMock2 = viewDocument.addObserver( ObserverMock );
-
-			expect( observerMock1 ).to.be.instanceof( ObserverMock );
-			expect( observerMock2 ).to.be.instanceof( ObserverMock );
-			expect( observerMock1 ).to.equals( observerMock2 );
-		} );
-
-		it( 'should instantiate one observer only once', () => {
-			viewDocument.addObserver( ObserverMockGlobalCount );
-			viewDocument.addObserver( ObserverMockGlobalCount );
-
-			expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
-			expect( instantiated ).to.equal( 1 );
-			expect( enabled ).to.equal( 1 );
-
-			viewDocument.addObserver( ObserverMock );
-			expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
-		} );
-
-		it( 'should instantiate child class of already registered observer', () => {
-			class ObserverMock extends Observer {
-				enable() {}
-			}
-			class ChildObserverMock extends ObserverMock {
-				enable() {}
-			}
-
-			viewDocument.addObserver( ObserverMock );
-			viewDocument.addObserver( ChildObserverMock );
-
-			expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
-		} );
-
-		it( 'should be disabled and re-enabled on render', () => {
-			const observerMock = viewDocument.addObserver( ObserverMock );
-			viewDocument.render();
-
-			sinon.assert.calledOnce( observerMock.disable );
-			sinon.assert.calledOnce( viewDocument.renderer.render );
-			sinon.assert.calledTwice( observerMock.enable );
-		} );
-
-		it( 'should call observe on each root', () => {
-			createViewRoot( viewDocument, 'div', 'roo1' );
-			createViewRoot( viewDocument, 'div', 'roo2' );
-
-			viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo1' );
-			viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo2' );
-
-			const observerMock = viewDocument.addObserver( ObserverMock );
-
-			sinon.assert.calledTwice( observerMock.observe );
-		} );
-	} );
-
-	describe( 'getObserver()', () => {
-		it( 'should return observer it it is added', () => {
-			const addedObserverMock = viewDocument.addObserver( ObserverMock );
-			const getObserverMock = viewDocument.getObserver( ObserverMock );
-
-			expect( getObserverMock ).to.be.instanceof( ObserverMock );
-			expect( getObserverMock ).to.equal( addedObserverMock );
-		} );
-
-		it( 'should return undefined if observer is not added', () => {
-			const getObserverMock = viewDocument.getObserver( ObserverMock );
-
-			expect( getObserverMock ).to.be.undefined;
-		} );
-	} );
-
-	describe( 'scrollToTheSelection()', () => {
-		beforeEach( () => {
-			// Silence the Rect warnings.
-			testUtils.sinon.stub( log, 'warn' );
-		} );
-
-		it( 'does nothing when there are no ranges in the selection', () => {
-			const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
-
-			viewDocument.scrollToTheSelection();
-			sinon.assert.notCalled( stub );
-		} );
-
-		it( 'scrolls to the first range in selection with an offset', () => {
-			const root = createViewRoot( viewDocument, 'div', 'main' );
-			const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
-			const range = ViewRange.createIn( root );
-
-			viewDocument.attachDomRoot( domRoot );
-
-			// Make sure the window will have to scroll to the domRoot.
-			Object.assign( domRoot.style, {
-				position: 'absolute',
-				top: '-1000px',
-				left: '-1000px'
-			} );
-
-			viewDocument.selection.setTo( range );
-
-			viewDocument.scrollToTheSelection();
-			sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
-		} );
-	} );
-
-	describe( 'disableObservers()', () => {
-		it( 'should disable observers', () => {
-			const addedObserverMock = viewDocument.addObserver( ObserverMock );
-
-			expect( addedObserverMock.enable.calledOnce ).to.be.true;
-			expect( addedObserverMock.disable.called ).to.be.false;
-
-			viewDocument.disableObservers();
-
-			expect( addedObserverMock.enable.calledOnce ).to.be.true;
-			expect( addedObserverMock.disable.calledOnce ).to.be.true;
-		} );
-	} );
-
-	describe( 'enableObservers()', () => {
-		it( 'should enable observers', () => {
-			const addedObserverMock = viewDocument.addObserver( ObserverMock );
-
-			viewDocument.disableObservers();
-
-			expect( addedObserverMock.enable.calledOnce ).to.be.true;
-			expect( addedObserverMock.disable.calledOnce ).to.be.true;
-
-			viewDocument.enableObservers();
-
-			expect( addedObserverMock.enable.calledTwice ).to.be.true;
-			expect( addedObserverMock.disable.calledOnce ).to.be.true;
-		} );
-	} );
-
-	describe( 'isFocused', () => {
-		it( 'should change renderer.isFocused too', () => {
-			expect( viewDocument.isFocused ).to.equal( false );
-			expect( viewDocument.renderer.isFocused ).to.equal( false );
-
-			viewDocument.isFocused = true;
-
-			expect( viewDocument.isFocused ).to.equal( true );
-			expect( viewDocument.renderer.isFocused ).to.equal( true );
-		} );
-	} );
-
-	describe( 'focus()', () => {
-		let domEditable, viewEditable;
-
-		beforeEach( () => {
-			domEditable = document.createElement( 'div' );
-			domEditable.setAttribute( 'contenteditable', 'true' );
-			document.body.appendChild( domEditable );
-			viewEditable = createViewRoot( viewDocument, 'div', 'main' );
-			viewDocument.attachDomRoot( domEditable );
-			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
-		} );
-
-		afterEach( () => {
-			document.body.removeChild( domEditable );
-		} );
-
-		it( 'should focus editable with selection', () => {
-			const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
-			const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
-
-			viewDocument.focus();
-
-			expect( converterFocusSpy.called ).to.be.true;
-			expect( renderSpy.calledOnce ).to.be.true;
-			expect( document.activeElement ).to.equal( domEditable );
-			const domSelection = document.getSelection();
-			expect( domSelection.rangeCount ).to.equal( 1 );
-			const domRange = domSelection.getRangeAt( 0 );
-			expect( domRange.startContainer ).to.equal( domEditable );
-			expect( domRange.startOffset ).to.equal( 0 );
-			expect( domRange.collapsed ).to.be.true;
-		} );
-
-		it( 'should not focus if document is already focused', () => {
-			const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
-			const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
-			viewDocument.isFocused = true;
-
-			viewDocument.focus();
-
-			expect( converterFocusSpy.called ).to.be.false;
-			expect( renderSpy.called ).to.be.false;
-		} );
-
-		it( 'should log warning when no selection', () => {
-			const logSpy = testUtils.sinon.stub( log, 'warn' );
-			viewDocument.selection.setTo( null );
-
-			viewDocument.focus();
-			expect( logSpy.calledOnce ).to.be.true;
-			expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^view-focus-no-selection/ );
-		} );
-	} );
-
-	describe( 'render()', () => {
-		it( 'should fire an event', () => {
-			const spy = sinon.spy();
-
-			viewDocument.on( 'render', spy );
-
-			viewDocument.render();
-
-			expect( spy.calledOnce ).to.be.true;
-		} );
-
-		it( 'disable observers, renders and enable observers', () => {
-			const observerMock = viewDocument.addObserver( ObserverMock );
-			const renderStub = sinon.stub( viewDocument.renderer, 'render' );
-
-			viewDocument.render();
-
-			sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable );
-		} );
-	} );
-} );

+ 0 - 83
packages/ckeditor5-engine/tests/view/document/integration.js

@@ -1,83 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals document */
-
-import RootEditableElement from '../../../src/view/rooteditableelement';
-import Document from '../../../src/view/document';
-import ViewElement from '../../../src/view/element';
-import { isBlockFiller, BR_FILLER } from '../../../src/view/filler';
-
-import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-
-describe( 'Document integration', () => {
-	it( 'should remove content of the DOM', () => {
-		const domDiv = createElement( document, 'div', { id: 'editor' }, [
-			createElement( document, 'p' ),
-			createElement( document, 'p' )
-		] );
-
-		const viewDocument = new Document();
-
-		createRoot( 'div', 'main', viewDocument );
-		viewDocument.attachDomRoot( domDiv );
-		viewDocument.render();
-
-		expect( domDiv.childNodes.length ).to.equal( 1 );
-		expect( isBlockFiller( domDiv.childNodes[ 0 ], BR_FILLER ) ).to.be.true;
-
-		viewDocument.destroy();
-	} );
-
-	it( 'should render changes in the Document', () => {
-		const domDiv = document.createElement( 'div' );
-
-		const viewDocument = new Document();
-		createRoot( 'div', 'main', viewDocument );
-		viewDocument.attachDomRoot( domDiv );
-
-		viewDocument.getRoot().appendChildren( new ViewElement( 'p' ) );
-		viewDocument.render();
-
-		expect( domDiv.childNodes.length ).to.equal( 1 );
-		expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );
-
-		viewDocument.destroy();
-	} );
-
-	it( 'should render attribute changes', () => {
-		const domRoot = document.createElement( 'div' );
-
-		const viewDocument = new Document();
-		const viewRoot = createRoot( 'div', 'main', viewDocument );
-
-		viewDocument.attachDomRoot( domRoot );
-
-		const viewP = new ViewElement( 'p', { class: 'foo' } );
-		viewRoot.appendChildren( viewP );
-		viewDocument.render();
-
-		expect( domRoot.childNodes.length ).to.equal( 1 );
-		expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' );
-
-		viewP.setAttribute( 'class', 'bar' );
-		viewDocument.render();
-
-		expect( domRoot.childNodes.length ).to.equal( 1 );
-		expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' );
-
-		viewDocument.destroy();
-	} );
-} );
-
-function createRoot( name, rootName, viewDoc ) {
-	const viewRoot = new RootEditableElement( name );
-
-	viewRoot.rootName = rootName;
-	viewRoot.document = viewDoc;
-	viewDoc.roots.add( viewRoot );
-
-	return viewRoot;
-}

+ 1 - 1
packages/ckeditor5-engine/tests/view/domconverter/binding.js

@@ -283,7 +283,7 @@ describe( 'DomConverter', () => {
 		it( 'should keep a copy of selection', () => {
 			const selectionCopy = new ViewSelection( selection );
 
-			selection.setTo( ViewRange.createIn( new ViewElement() ), true );
+			selection._setTo( ViewRange.createIn( new ViewElement() ), true );
 			const bindSelection = converter.fakeSelectionToView( domEl );
 
 			expect( bindSelection ).to.not.equal( selection );

+ 1 - 2
packages/ckeditor5-engine/tests/view/domconverter/domconverter.js

@@ -40,7 +40,7 @@ describe( 'DomConverter', () => {
 		beforeEach( () => {
 			viewDocument = new ViewDocument();
 			viewEditable = new ViewEditable( 'div' );
-			viewEditable.document = viewDocument;
+			viewEditable._document = viewDocument;
 
 			domEditable = document.createElement( 'div' );
 			domEditableParent = document.createElement( 'div' );
@@ -53,7 +53,6 @@ describe( 'DomConverter', () => {
 		afterEach( () => {
 			converter.unbindDomElement( domEditable );
 			document.body.removeChild( domEditableParent );
-			viewDocument.destroy();
 
 			document.body.focus();
 		} );

+ 3 - 9
packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js

@@ -29,9 +29,7 @@ describe( 'DomConverter', () => {
 		it( 'should create tree of DOM elements from view elements', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
-			const viewP = new ViewElement( 'p' );
-
-			viewP.setAttribute( 'class', 'foo' );
+			const viewP = new ViewElement( 'p', { class: 'foo' } );
 
 			viewP.appendChildren( viewImg );
 			viewP.appendChildren( viewText );
@@ -59,9 +57,7 @@ describe( 'DomConverter', () => {
 		it( 'should create tree of DOM elements from view elements and bind elements', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
-			const viewP = new ViewElement( 'p' );
-
-			viewP.setAttribute( 'class', 'foo' );
+			const viewP = new ViewElement( 'p', { class: 'foo' } );
 
 			viewP.appendChildren( viewImg );
 			viewP.appendChildren( viewText );
@@ -98,9 +94,7 @@ describe( 'DomConverter', () => {
 		it( 'should create tree of DOM elements from view element without children', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
-			const viewP = new ViewElement( 'p' );
-
-			viewP.setAttribute( 'class', 'foo' );
+			const viewP = new ViewElement( 'p', { class: 'foo' } );
 
 			viewP.appendChildren( viewImg );
 			viewP.appendChildren( viewText );

+ 15 - 38
packages/ckeditor5-engine/tests/view/editableelement.js

@@ -18,7 +18,7 @@ describe( 'EditableElement', () => {
 		} );
 
 		it( 'should allow to set document', () => {
-			element.document = docMock;
+			element._document = docMock;
 
 			expect( element.document ).to.equal( docMock );
 		} );
@@ -28,16 +28,16 @@ describe( 'EditableElement', () => {
 		} );
 
 		it( 'should throw if trying to set document again', () => {
-			element.document = docMock;
+			element._document = docMock;
 			const newDoc = createDocumentMock();
 
 			expect( () => {
-				element.document = newDoc;
+				element._document = newDoc;
 			} ).to.throw( CKEditorError, 'view-editableelement-document-already-set: View document is already set.' );
 		} );
 
 		it( 'should be cloned properly', () => {
-			element.document = docMock;
+			element._document = docMock;
 			const newElement = element.clone();
 
 			expect( newElement.document ).to.equal( docMock );
@@ -51,16 +51,16 @@ describe( 'EditableElement', () => {
 			docMock = createDocumentMock();
 
 			viewMain = new RootEditableElement( 'div' );
-			viewMain.document = docMock;
+			viewMain._document = docMock;
 
 			viewHeader = new RootEditableElement( 'h1' );
-			viewHeader.document = docMock;
+			viewHeader._document = docMock;
 			viewHeader.rootName = 'header';
 		} );
 
 		it( 'should be observable', () => {
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 			expect( root.isFocused ).to.be.false;
 
@@ -75,48 +75,25 @@ describe( 'EditableElement', () => {
 			expect( isFocusedSpy.calledOnce ).to.be.true;
 		} );
 
-		it( 'should change isFocused on document render event', () => {
+		it( 'should change isFocused when selection changes', () => {
 			const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
-			docMock.selection.setTo( rangeMain );
+			docMock.selection._setTo( rangeMain );
 			docMock.isFocused = true;
 
 			expect( viewMain.isFocused ).to.be.true;
 			expect( viewHeader.isFocused ).to.be.false;
 
-			docMock.selection.setTo( [ rangeHeader ] );
-			docMock.fire( 'render' );
+			docMock.selection._setTo( [ rangeHeader ] );
 
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.true;
 		} );
 
-		it( 'should change isFocus before actual rendering', done => {
-			const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
-			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
-			docMock.render = sinon.spy();
-
-			docMock.selection.setTo( rangeMain );
-			docMock.isFocused = true;
-
-			expect( viewMain.isFocused ).to.be.true;
-			expect( viewHeader.isFocused ).to.be.false;
-
-			docMock.selection.setTo( [ rangeHeader ] );
-
-			viewHeader.on( 'change:isFocused', ( evt, propertyName, value ) => {
-				expect( value ).to.be.true;
-				sinon.assert.notCalled( docMock.render );
-				done();
-			} );
-
-			docMock.fire( 'render' );
-		} );
-
 		it( 'should change isFocused when document.isFocus changes', () => {
 			const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
-			docMock.selection.setTo( rangeMain );
+			docMock.selection._setTo( rangeMain );
 			docMock.isFocused = true;
 
 			expect( viewMain.isFocused ).to.be.true;
@@ -127,7 +104,7 @@ describe( 'EditableElement', () => {
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.false;
 
-			docMock.selection.setTo( [ rangeHeader ] );
+			docMock.selection._setTo( [ rangeHeader ] );
 
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.false;
@@ -137,7 +114,7 @@ describe( 'EditableElement', () => {
 	describe( 'isReadOnly', () => {
 		it( 'should be observable', () => {
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 			expect( root.isReadOnly ).to.be.false;
 
@@ -154,7 +131,7 @@ describe( 'EditableElement', () => {
 
 		it( 'should be bound to the document#isReadOnly', () => {
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 			root.document.isReadOnly = false;
 
@@ -170,7 +147,7 @@ describe( 'EditableElement', () => {
 		it( 'should return document', () => {
 			const docMock = createDocumentMock();
 			const root = new RootEditableElement( 'div' );
-			root.document = docMock;
+			root._document = docMock;
 
 			expect( root.document ).to.equal( docMock );
 		} );

+ 98 - 98
packages/ckeditor5-engine/tests/view/element.js

@@ -174,7 +174,7 @@ describe( 'Element', () => {
 
 		it( 'should clone class attribute', () => {
 			const el = new Element( 'p', { foo: 'bar' } );
-			el.addClass( 'baz', 'qux' );
+			el._addClass( [ 'baz', 'qux' ] );
 			const clone = el.clone( false );
 
 			expect( clone ).to.not.equal( el );
@@ -198,8 +198,8 @@ describe( 'Element', () => {
 		it( 'should clone custom properties', () => {
 			const el = new Element( 'p' );
 			const symbol = Symbol( 'custom' );
-			el.setCustomProperty( 'foo', 'bar' );
-			el.setCustomProperty( symbol, 'baz' );
+			el._setCustomProperty( 'foo', 'bar' );
+			el._setCustomProperty( symbol, 'baz' );
 
 			const cloned = el.clone();
 
@@ -247,9 +247,9 @@ describe( 'Element', () => {
 			const other1 = el.clone();
 			const other2 = el.clone();
 			const other3 = el.clone();
-			other1.setAttribute( 'baz', 'qux' );
-			other2.setAttribute( 'foo', 'not-bar' );
-			other3.removeAttribute( 'foo' );
+			other1._setAttribute( 'baz', 'qux' );
+			other2._setAttribute( 'foo', 'not-bar' );
+			other3._removeAttribute( 'foo' );
 			expect( el.isSimilar( other1 ) ).to.be.false;
 			expect( el.isSimilar( other2 ) ).to.be.false;
 			expect( el.isSimilar( other3 ) ).to.be.false;
@@ -261,10 +261,10 @@ describe( 'Element', () => {
 			const el3 = new Element( 'p' );
 			const el4 = new Element( 'p' );
 
-			el1.addClass( 'foo', 'bar' );
-			el2.addClass( 'bar', 'foo' );
-			el3.addClass( 'baz' );
-			el4.addClass( 'baz', 'bar' );
+			el1._addClass( [ 'foo', 'bar' ] );
+			el2._addClass( [ 'bar', 'foo' ] );
+			el3._addClass( 'baz' );
+			el4._addClass( [ 'baz', 'bar' ] );
 
 			expect( el1.isSimilar( el2 ) ).to.be.true;
 			expect( el1.isSimilar( el3 ) ).to.be.false;
@@ -277,13 +277,13 @@ describe( 'Element', () => {
 			const el3 = new Element( 'p' );
 			const el4 = new Element( 'p' );
 
-			el1.setStyle( 'color', 'red' );
-			el1.setStyle( 'top', '10px' );
-			el2.setStyle( 'top', '20px' );
-			el3.setStyle( 'top', '10px' );
-			el3.setStyle( 'color', 'red' );
-			el4.setStyle( 'color', 'blue' );
-			el4.setStyle( 'top', '10px' );
+			el1._setStyle( 'color', 'red' );
+			el1._setStyle( 'top', '10px' );
+			el2._setStyle( 'top', '20px' );
+			el3._setStyle( 'top', '10px' );
+			el3._setStyle( 'color', 'red' );
+			el4._setStyle( 'color', 'blue' );
+			el4._setStyle( 'top', '10px' );
 
 			expect( el1.isSimilar( el2 ) ).to.be.false;
 			expect( el1.isSimilar( el3 ) ).to.be.true;
@@ -430,16 +430,16 @@ describe( 'Element', () => {
 			el = new Element( 'p' );
 		} );
 
-		describe( 'setAttribute', () => {
+		describe( '_setAttribute', () => {
 			it( 'should set attribute', () => {
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 
 				expect( el._attrs.has( 'foo' ) ).to.be.true;
 				expect( el._attrs.get( 'foo' ) ).to.equal( 'bar' );
 			} );
 
 			it( 'should cast attribute value to a string', () => {
-				el.setAttribute( 'foo', true );
+				el._setAttribute( 'foo', true );
 
 				expect( el._attrs.get( 'foo' ) ).to.equal( 'true' );
 			} );
@@ -450,11 +450,11 @@ describe( 'Element', () => {
 					done();
 				} );
 
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 			} );
 
 			it( 'should set class', () => {
-				el.setAttribute( 'class', 'foo bar' );
+				el._setAttribute( 'class', 'foo bar' );
 
 				expect( el._attrs.has( 'class' ) ).to.be.false;
 				expect( el._classes.has( 'foo' ) ).to.be.true;
@@ -462,8 +462,8 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should replace all existing classes', () => {
-				el.setAttribute( 'class', 'foo bar baz' );
-				el.setAttribute( 'class', 'qux' );
+				el._setAttribute( 'class', 'foo bar baz' );
+				el._setAttribute( 'class', 'qux' );
 
 				expect( el._classes.has( 'foo' ) ).to.be.false;
 				expect( el._classes.has( 'bar' ) ).to.be.false;
@@ -472,9 +472,9 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should replace all styles', () => {
-				el.setStyle( 'color', 'red' );
-				el.setStyle( 'top', '10px' );
-				el.setAttribute( 'style', 'border:none' );
+				el._setStyle( 'color', 'red' );
+				el._setStyle( 'top', '10px' );
+				el._setAttribute( 'style', 'border:none' );
 
 				expect( el.hasStyle( 'color' ) ).to.be.false;
 				expect( el.hasStyle( 'top' ) ).to.be.false;
@@ -485,14 +485,14 @@ describe( 'Element', () => {
 
 		describe( 'getAttribute', () => {
 			it( 'should return attribute', () => {
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 
 				expect( el.getAttribute( 'foo' ) ).to.equal( 'bar' );
 				expect( el.getAttribute( 'bom' ) ).to.not.be.ok;
 			} );
 
 			it( 'should return class attribute', () => {
-				el.addClass( 'foo', 'bar' );
+				el._addClass( [ 'foo', 'bar' ] );
 
 				expect( el.getAttribute( 'class' ) ).to.equal( 'foo bar' );
 			} );
@@ -502,8 +502,8 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should return style attribute', () => {
-				el.setStyle( 'color', 'red' );
-				el.setStyle( 'top', '10px' );
+				el._setStyle( 'color', 'red' );
+				el._setStyle( 'top', '10px' );
 
 				expect( el.getAttribute( 'style' ) ).to.equal( 'color:red;top:10px;' );
 			} );
@@ -515,17 +515,17 @@ describe( 'Element', () => {
 
 		describe( 'getAttributes', () => {
 			it( 'should return attributes', () => {
-				el.setAttribute( 'foo', 'bar' );
-				el.setAttribute( 'abc', 'xyz' );
+				el._setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'abc', 'xyz' );
 
 				expect( Array.from( el.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'abc', 'xyz' ] ] );
 			} );
 
 			it( 'should return class and style attribute', () => {
-				el.setAttribute( 'class', 'abc' );
-				el.setAttribute( 'style', 'width:20px;' );
-				el.addClass( 'xyz' );
-				el.setStyle( 'font-weight', 'bold' );
+				el._setAttribute( 'class', 'abc' );
+				el._setAttribute( 'style', 'width:20px;' );
+				el._addClass( 'xyz' );
+				el._setStyle( 'font-weight', 'bold' );
 
 				expect( Array.from( el.getAttributes() ) ).to.deep.equal( [
 					[ 'class', 'abc xyz' ], [ 'style', 'width:20px;font-weight:bold;' ]
@@ -535,7 +535,7 @@ describe( 'Element', () => {
 
 		describe( 'hasAttribute', () => {
 			it( 'should return true if element has attribute', () => {
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 
 				expect( el.hasAttribute( 'foo' ) ).to.be.true;
 				expect( el.hasAttribute( 'bom' ) ).to.be.false;
@@ -543,21 +543,21 @@ describe( 'Element', () => {
 
 			it( 'should return true if element has class attribute', () => {
 				expect( el.hasAttribute( 'class' ) ).to.be.false;
-				el.addClass( 'foo' );
+				el._addClass( 'foo' );
 				expect( el.hasAttribute( 'class' ) ).to.be.true;
 			} );
 
 			it( 'should return true if element has style attribute', () => {
 				expect( el.hasAttribute( 'style' ) ).to.be.false;
-				el.setStyle( 'border', '1px solid red' );
+				el._setStyle( 'border', '1px solid red' );
 				expect( el.hasAttribute( 'style' ) ).to.be.true;
 			} );
 		} );
 
 		describe( 'getAttributeKeys', () => {
 			it( 'should return keys', () => {
-				el.setAttribute( 'foo', true );
-				el.setAttribute( 'bar', true );
+				el._setAttribute( 'foo', true );
+				el._setAttribute( 'bar', true );
 
 				const expected = [ 'foo', 'bar' ];
 				let i = 0;
@@ -571,8 +571,8 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should return class key', () => {
-				el.addClass( 'foo' );
-				el.setAttribute( 'bar', true );
+				el._addClass( 'foo' );
+				el._setAttribute( 'bar', true );
 				const expected = [ 'class', 'bar' ];
 				let i = 0;
 
@@ -583,8 +583,8 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should return style key', () => {
-				el.setStyle( 'color', 'black' );
-				el.setAttribute( 'bar', true );
+				el._setStyle( 'color', 'black' );
+				el._setAttribute( 'bar', true );
 				const expected = [ 'style', 'bar' ];
 				let i = 0;
 
@@ -595,13 +595,13 @@ describe( 'Element', () => {
 			} );
 		} );
 
-		describe( 'removeAttribute', () => {
+		describe( '_removeAttribute', () => {
 			it( 'should remove attributes', () => {
-				el.setAttribute( 'foo', true );
+				el._setAttribute( 'foo', true );
 
 				expect( el.hasAttribute( 'foo' ) ).to.be.true;
 
-				el.removeAttribute( 'foo' );
+				el._removeAttribute( 'foo' );
 
 				expect( el.hasAttribute( 'foo' ) ).to.be.false;
 
@@ -609,20 +609,20 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should fire change event with attributes type', done => {
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 				el.once( 'change:attributes', eventInfo => {
 					expect( eventInfo.source ).to.equal( el );
 					done();
 				} );
 
-				el.removeAttribute( 'foo' );
+				el._removeAttribute( 'foo' );
 			} );
 
 			it( 'should remove class attribute', () => {
-				el.addClass( 'foo', 'bar' );
+				el._addClass( [ 'foo', 'bar' ] );
 				const el2 = new Element( 'p' );
-				const removed1 = el.removeAttribute( 'class' );
-				const removed2 = el2.removeAttribute( 'class' );
+				const removed1 = el._removeAttribute( 'class' );
+				const removed2 = el2._removeAttribute( 'class' );
 
 				expect( el.hasAttribute( 'class' ) ).to.be.false;
 				expect( el.hasClass( 'foo' ) ).to.be.false;
@@ -632,11 +632,11 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should remove style attribute', () => {
-				el.setStyle( 'color', 'red' );
-				el.setStyle( 'position', 'fixed' );
+				el._setStyle( 'color', 'red' );
+				el._setStyle( 'position', 'fixed' );
 				const el2 = new Element( 'p' );
-				const removed1 = el.removeAttribute( 'style' );
-				const removed2 = el2.removeAttribute( 'style' );
+				const removed1 = el._removeAttribute( 'style' );
+				const removed2 = el2._removeAttribute( 'style' );
 
 				expect( el.hasAttribute( 'style' ) ).to.be.false;
 				expect( el.hasStyle( 'color' ) ).to.be.false;
@@ -654,9 +654,9 @@ describe( 'Element', () => {
 			el = new Element( 'p' );
 		} );
 
-		describe( 'addClass', () => {
+		describe( '_addClass()', () => {
 			it( 'should add single class', () => {
-				el.addClass( 'one' );
+				el._addClass( 'one' );
 
 				expect( el._classes.has( 'one' ) ).to.be.true;
 			} );
@@ -667,11 +667,11 @@ describe( 'Element', () => {
 					done();
 				} );
 
-				el.addClass( 'one' );
+				el._addClass( 'one' );
 			} );
 
 			it( 'should add multiple classes', () => {
-				el.addClass( 'one', 'two', 'three' );
+				el._addClass( [ 'one', 'two', 'three' ] );
 
 				expect( el._classes.has( 'one' ) ).to.be.true;
 				expect( el._classes.has( 'two' ) ).to.be.true;
@@ -679,11 +679,11 @@ describe( 'Element', () => {
 			} );
 		} );
 
-		describe( 'removeClass', () => {
+		describe( '_removeClass()', () => {
 			it( 'should remove single class', () => {
-				el.addClass( 'one', 'two', 'three' );
+				el._addClass( [ 'one', 'two', 'three' ] );
 
-				el.removeClass( 'one' );
+				el._removeClass( 'one' );
 
 				expect( el._classes.has( 'one' ) ).to.be.false;
 				expect( el._classes.has( 'two' ) ).to.be.true;
@@ -691,18 +691,18 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should fire change event with attributes type', done => {
-				el.addClass( 'one' );
+				el._addClass( 'one' );
 				el.once( 'change:attributes', eventInfo => {
 					expect( eventInfo.source ).to.equal( el );
 					done();
 				} );
 
-				el.removeClass( 'one' );
+				el._removeClass( 'one' );
 			} );
 
 			it( 'should remove multiple classes', () => {
-				el.addClass( 'one', 'two', 'three', 'four' );
-				el.removeClass( 'one', 'two', 'three' );
+				el._addClass( [ 'one', 'two', 'three', 'four' ] );
+				el._removeClass( [ 'one', 'two', 'three' ] );
 
 				expect( el._classes.has( 'one' ) ).to.be.false;
 				expect( el._classes.has( 'two' ) ).to.be.false;
@@ -713,7 +713,7 @@ describe( 'Element', () => {
 
 		describe( 'hasClass', () => {
 			it( 'should check if element has a class', () => {
-				el.addClass( 'one', 'two', 'three' );
+				el._addClass( [ 'one', 'two', 'three' ] );
 
 				expect( el.hasClass( 'one' ) ).to.be.true;
 				expect( el.hasClass( 'two' ) ).to.be.true;
@@ -722,7 +722,7 @@ describe( 'Element', () => {
 			} );
 
 			it( 'should check if element has multiple classes', () => {
-				el.addClass( 'one', 'two', 'three' );
+				el._addClass( [ 'one', 'two', 'three' ] );
 
 				expect( el.hasClass( 'one', 'two' ) ).to.be.true;
 				expect( el.hasClass( 'three', 'two' ) ).to.be.true;
@@ -734,7 +734,7 @@ describe( 'Element', () => {
 		describe( 'getClassNames', () => {
 			it( 'should return iterator with all class names', () => {
 				const names = [ 'one', 'two', 'three' ];
-				el.addClass( ...names );
+				el._addClass( names );
 				const iterator = el.getClassNames();
 				let i = 0;
 
@@ -752,9 +752,9 @@ describe( 'Element', () => {
 			el = new Element( 'p' );
 		} );
 
-		describe( 'setStyle', () => {
+		describe( '_setStyle()', () => {
 			it( 'should set element style', () => {
-				el.setStyle( 'color', 'red' );
+				el._setStyle( 'color', 'red' );
 
 				expect( el._styles.has( 'color' ) ).to.be.true;
 				expect( el._styles.get( 'color' ) ).to.equal( 'red' );
@@ -766,11 +766,11 @@ describe( 'Element', () => {
 					done();
 				} );
 
-				el.setStyle( 'color', 'red' );
+				el._setStyle( 'color', 'red' );
 			} );
 
 			it( 'should set multiple styles by providing an object', () => {
-				el.setStyle( {
+				el._setStyle( {
 					color: 'red',
 					position: 'fixed'
 				} );
@@ -784,7 +784,7 @@ describe( 'Element', () => {
 
 		describe( 'getStyle', () => {
 			it( 'should get style', () => {
-				el.setStyle( {
+				el._setStyle( {
 					color: 'red',
 					border: '1px solid red'
 				} );
@@ -798,7 +798,7 @@ describe( 'Element', () => {
 			it( 'should return iterator with all style names', () => {
 				const names = [ 'color', 'position' ];
 
-				el.setStyle( {
+				el._setStyle( {
 					color: 'red',
 					position: 'absolute'
 				} );
@@ -814,14 +814,14 @@ describe( 'Element', () => {
 
 		describe( 'hasStyle', () => {
 			it( 'should check if element has a style', () => {
-				el.setStyle( 'padding-top', '10px' );
+				el._setStyle( 'padding-top', '10px' );
 
 				expect( el.hasStyle( 'padding-top' ) ).to.be.true;
 				expect( el.hasStyle( 'padding-left' ) ).to.be.false;
 			} );
 
 			it( 'should check if element has multiple styles', () => {
-				el.setStyle( {
+				el._setStyle( {
 					'padding-top': '10px',
 					'margin-left': '10px',
 					'color': '10px;'
@@ -833,31 +833,31 @@ describe( 'Element', () => {
 			} );
 		} );
 
-		describe( 'removeStyle', () => {
+		describe( '_removeStyle()', () => {
 			it( 'should remove style', () => {
-				el.setStyle( 'padding-top', '10px' );
-				el.removeStyle( 'padding-top' );
+				el._setStyle( 'padding-top', '10px' );
+				el._removeStyle( 'padding-top' );
 
 				expect( el.hasStyle( 'padding-top' ) ).to.be.false;
 			} );
 
 			it( 'should fire change event with attributes type', done => {
-				el.setStyle( 'color', 'red' );
+				el._setStyle( 'color', 'red' );
 				el.once( 'change:attributes', eventInfo => {
 					expect( eventInfo.source ).to.equal( el );
 					done();
 				} );
 
-				el.removeStyle( 'color' );
+				el._removeStyle( 'color' );
 			} );
 
 			it( 'should remove multiple styles', () => {
-				el.setStyle( {
+				el._setStyle( {
 					'padding-top': '10px',
 					'margin-top': '10px',
 					'color': 'red'
 				} );
-				el.removeStyle( 'padding-top', 'margin-top' );
+				el._removeStyle( [ 'padding-top', 'margin-top' ] );
 
 				expect( el.hasStyle( 'padding-top' ) ).to.be.false;
 				expect( el.hasStyle( 'margin-top' ) ).to.be.false;
@@ -964,7 +964,7 @@ describe( 'Element', () => {
 	describe( 'custom properties', () => {
 		it( 'should allow to set and get custom properties', () => {
 			const el = new Element( 'p' );
-			el.setCustomProperty( 'foo', 'bar' );
+			el._setCustomProperty( 'foo', 'bar' );
 
 			expect( el.getCustomProperty( 'foo' ) ).to.equal( 'bar' );
 		} );
@@ -972,7 +972,7 @@ describe( 'Element', () => {
 		it( 'should allow to add symbol property', () => {
 			const el = new Element( 'p' );
 			const symbol = Symbol( 'custom' );
-			el.setCustomProperty( symbol, 'bar' );
+			el._setCustomProperty( symbol, 'bar' );
 
 			expect( el.getCustomProperty( symbol ) ).to.equal( 'bar' );
 		} );
@@ -980,14 +980,14 @@ describe( 'Element', () => {
 		it( 'should allow to remove custom property', () => {
 			const el = new Element( 'foo' );
 			const symbol = Symbol( 'quix' );
-			el.setCustomProperty( 'bar', 'baz' );
-			el.setCustomProperty( symbol, 'test' );
+			el._setCustomProperty( 'bar', 'baz' );
+			el._setCustomProperty( symbol, 'test' );
 
 			expect( el.getCustomProperty( 'bar' ) ).to.equal( 'baz' );
 			expect( el.getCustomProperty( symbol ) ).to.equal( 'test' );
 
-			el.removeCustomProperty( 'bar' );
-			el.removeCustomProperty( symbol );
+			el._removeCustomProperty( 'bar' );
+			el._removeCustomProperty( symbol );
 
 			expect( el.getCustomProperty( 'bar' ) ).to.be.undefined;
 			expect( el.getCustomProperty( symbol ) ).to.be.undefined;
@@ -995,9 +995,9 @@ describe( 'Element', () => {
 
 		it( 'should allow to iterate over custom properties', () => {
 			const el = new Element( 'p' );
-			el.setCustomProperty( 'foo', 1 );
-			el.setCustomProperty( 'bar', 2 );
-			el.setCustomProperty( 'baz', 3 );
+			el._setCustomProperty( 'foo', 1 );
+			el._setCustomProperty( 'bar', 2 );
+			el._setCustomProperty( 'baz', 3 );
 
 			const properties = [ ...el.getCustomProperties() ];
 
@@ -1019,7 +1019,7 @@ describe( 'Element', () => {
 
 		it( 'should return classes in sorted order', () => {
 			const el = new Element( 'fruit' );
-			el.addClass( 'banana', 'lemon', 'apple' );
+			el._addClass( [ 'banana', 'lemon', 'apple' ] );
 
 			expect( el.getIdentity() ).to.equal( 'fruit class="apple,banana,lemon"' );
 		} );
@@ -1049,7 +1049,7 @@ describe( 'Element', () => {
 				style: 'text-align:center;border-radius:10px'
 			} );
 
-			el.addClass( 'three', 'two', 'one' );
+			el._addClass( [ 'three', 'two', 'one' ] );
 
 			expect( el.getIdentity() ).to.equal(
 				'baz class="one,three,two" style="border-radius:10px;text-align:center" bar="two" foo="one"'

+ 17 - 16
packages/ckeditor5-engine/tests/view/manual/clickobserver.js

@@ -5,15 +5,19 @@
 
 /* globals console, document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
+const view = new View();
+const viewDocument = view.document;
+
+// Disable rendering for this example, because it re-enables all observers each time view is rendered.
+view.render = () => {};
 
 class ClickObserver1 extends DomEventObserver {
-	constructor( viewDocument ) {
-		super( viewDocument );
+	constructor( view ) {
+		super( view );
 
 		this.id = 1;
 		this.domEventType = 'click';
@@ -25,8 +29,8 @@ class ClickObserver1 extends DomEventObserver {
 }
 
 class ClickObserver2 extends DomEventObserver {
-	constructor( viewDocument ) {
-		super( viewDocument );
+	constructor( view ) {
+		super( view );
 
 		this.id = 2;
 		this.domEventType = 'click';
@@ -37,19 +41,16 @@ class ClickObserver2 extends DomEventObserver {
 	}
 }
 
-const observer1 = new ClickObserver1( viewDocument );
-
 viewDocument.on( 'click', ( evt, evtData ) => console.log( 'click', evtData.id, evtData.domTarget.id ) );
-document.getElementById( 'enable1' ).addEventListener( 'click', () => observer1.enable() );
-document.getElementById( 'disable1' ).addEventListener( 'click', () => observer1.disable() );
 
 // Random order.
-viewDocument.addObserver( ClickObserver1 );
-
+view.addObserver( ClickObserver1 );
 createViewRoot( viewDocument, 'div', 'clickerA' );
-viewDocument.attachDomRoot( document.getElementById( 'clickerA' ), 'clickerA' );
-
-viewDocument.addObserver( ClickObserver2 );
+view.attachDomRoot( document.getElementById( 'clickerA' ), 'clickerA' );
 
+view.addObserver( ClickObserver2 );
 createViewRoot( viewDocument, 'div', 'clickerB' );
-viewDocument.attachDomRoot( document.getElementById( 'clickerB' ), 'clickerB' );
+view.attachDomRoot( document.getElementById( 'clickerB' ), 'clickerB' );
+
+document.getElementById( 'enable1' ).addEventListener( 'click', () => view.getObserver( ClickObserver1 ).enable() );
+document.getElementById( 'disable1' ).addEventListener( 'click', () => view.getObserver( ClickObserver1 ).disable() );

+ 19 - 20
packages/ckeditor5-engine/tests/view/manual/fakeselection.js

@@ -5,21 +5,22 @@
 
 /* globals document, console */
 
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import ViewRange from '../../../src/view/range';
 import createViewRoot from '../_utils/createroot';
 import { setData } from '../../../src/dev-utils/view';
 
-const viewDocument = new ViewDocument();
+const view = new View();
+const viewDocument = view.document;
 const domEditable = document.getElementById( 'editor' );
-const viewRoot = createViewRoot();
+const viewRoot = createViewRoot( viewDocument );
 let viewStrong;
 
-viewDocument.attachDomRoot( domEditable );
+view.attachDomRoot( domEditable );
 
 // Add mouseup oberver.
-viewDocument.addObserver( class extends DomEventObserver {
+view.addObserver( class extends DomEventObserver {
 	get domEventType() {
 		return [ 'mousedown', 'mouseup' ];
 	}
@@ -30,19 +31,19 @@ viewDocument.addObserver( class extends DomEventObserver {
 } );
 
 viewDocument.on( 'selectionChange', ( evt, data ) => {
-	viewDocument.selection.setTo( data.newSelection );
-	viewDocument.render();
+	view.change( writer => {
+		writer.setSelection( data.newSelection );
+	} );
 } );
 
 viewDocument.on( 'mouseup', ( evt, data ) => {
 	if ( data.target == viewStrong ) {
 		console.log( 'Making selection around the <strong>.' );
 
-		const range = ViewRange.createOn( viewStrong );
-		viewDocument.selection.setTo( [ range ] );
-		viewDocument.selection.setFake( true, { label: 'fake selection over bar' } );
-
-		viewDocument.render();
+		view.change( writer => {
+			writer.setSelection( ViewRange.createOn( viewStrong ) );
+			writer.setFakeSelection( true, { label: 'fake selection over bar' } );
+		} );
 
 		data.preventDefault();
 	}
@@ -57,28 +58,26 @@ viewDocument.selection.on( 'change', () => {
 	const lastPos = viewDocument.selection.getLastPosition();
 
 	if ( firstPos && lastPos && firstPos.nodeAfter == viewStrong && lastPos.nodeBefore == viewStrong ) {
-		viewStrong.addClass( 'selected' );
+		view.change( writer => writer.addClass( 'selected', viewStrong ) );
 	} else {
-		viewStrong.removeClass( 'selected' );
+		view.change( writer => writer.removeClass( 'selected', viewStrong ) );
 	}
 } );
 
 viewDocument.on( 'focus', () => {
-	viewStrong.addClass( 'focused' );
-	viewDocument.render();
+	view.change( writer => writer.addClass( 'focused', viewStrong ) );
 
 	console.log( 'The document was focused.' );
 } );
 
 viewDocument.on( 'blur', () => {
-	viewStrong.removeClass( 'focused' );
-	viewDocument.render();
+	view.change( writer => writer.removeClass( 'focused', viewStrong ) );
 
 	console.log( 'The document was blurred.' );
 } );
 
-setData( viewDocument, '<container:p>{}foo<strong contenteditable="false">bar</strong>baz</container:p>' );
+setData( view, '<container:p>{}foo<strong contenteditable="false">bar</strong>baz</container:p>' );
 const viewP = viewRoot.getChild( 0 );
 viewStrong = viewP.getChild( 1 );
 
-viewDocument.focus();
+view.focus();

+ 29 - 15
packages/ckeditor5-engine/tests/view/manual/focus.js

@@ -5,32 +5,46 @@
 
 /* globals document */
 
-import Document from '../../../src/view/document';
-import { parse } from '../../../src/dev-utils/view';
+import View from '../../../src/view/view';
+import ViewPosition from '../../../src/view/position';
+import ViewRange from '../../../src/view/range';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
+const view = new View();
+const viewDocument = view.document;
 
 const domEditable1 = document.getElementById( 'editable1' );
 const domEditable2 = document.getElementById( 'editable2' );
 
-const editable1 = viewDocument.createRoot( domEditable1, 'editable1' );
-const editable2 = viewDocument.createRoot( domEditable2, 'editable2' );
+const editable1 = createViewRoot( viewDocument, 'div', 'editable1' );
+view.attachDomRoot( domEditable1, 'editable1' );
 
-viewDocument.on( 'selectionChange', ( evt, data ) => {
-	viewDocument.selection.setTo( data.newSelection );
-} );
+const editable2 = createViewRoot( viewDocument, 'div', 'editable2' );
+view.attachDomRoot( domEditable2, 'editable2' );
+
+let text1, text2;
+
+view.change( writer => {
+	text1 = writer.createText( 'Foo bar baz' );
+	text2 = writer.createText( 'Foo bar baz' );
 
-const { selection: selection1 } = parse( '<p>Foo {bar} baz.</p>', { rootElement: editable1 } );
-const { selection: selection2 } = parse( '<p>{Foo} bar baz.</p>', { rootElement: editable2 } );
+	writer.insert( ViewPosition.createAt( editable1 ), text1 );
+	writer.insert( ViewPosition.createAt( editable2 ), text2 );
+} );
 
 document.getElementById( 'button1' ).addEventListener( 'click', () => {
-	viewDocument.selection.setTo( selection1 );
-	viewDocument.focus();
+	view.change( writer => {
+		writer.setSelection( ViewRange.createFromParentsAndOffsets( text1, 4, text1, 7 ) );
+	} );
+
+	view.focus();
 } );
 
 document.getElementById( 'button2' ).addEventListener( 'click', () => {
-	viewDocument.selection.setTo( selection2 );
-	viewDocument.focus();
+	view.change( writer => {
+		writer.setSelection( ViewRange.createFromParentsAndOffsets( text2, 0, text2, 3 ) );
+	} );
+
+	view.focus();
 } );
 
-viewDocument.render();

+ 21 - 10
packages/ckeditor5-engine/tests/view/manual/focusobserver.js

@@ -5,10 +5,12 @@
 
 /* globals console, document */
 
-import Document from '../../../src/view/document';
-import { setData } from '../../../src/dev-utils/view';
+import View from '../../../src/view/view';
+import Position from '../../../src/view/position';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
+const view = new View();
+const viewDocument = view.document;
 
 viewDocument.on( 'focus', ( evt, data ) => console.log( `Focus in ${ data.domTarget.id }.` ) );
 viewDocument.on( 'blur', ( evt, data ) => console.log( `Blur in ${ data.domTarget.id }.` ) );
@@ -16,22 +18,31 @@ viewDocument.on( 'blur', ( evt, data ) => console.log( `Blur in ${ data.domTarge
 const domEditable1 = document.getElementById( 'editable1' );
 const domEditable2 = document.getElementById( 'editable2' );
 
-const editable1 = viewDocument.createRoot( domEditable1, 'editable1' );
-const editable2 = viewDocument.createRoot( domEditable2, 'editable2' );
+const editable1 = createViewRoot( viewDocument, 'div', 'editable1' );
+const editable2 = createViewRoot( viewDocument, 'div', 'editable2' );
+
+view.attachDomRoot( domEditable1, 'editable1' );
+view.attachDomRoot( domEditable2, 'editable2' );
 
 viewDocument.on( 'selectionChange', ( evt, data ) => {
-	viewDocument.selection.setTo( data.newSelection );
-	viewDocument.render();
+	view.change( writer => {
+		writer.setSelection( data.newSelection );
+	} );
 } );
 
-setData( viewDocument, '<container:p>{}First editable.</container:p>', { rootName: 'editable1' } );
-setData( viewDocument, '<container:p>Second editable.</container:p>', { rootName: 'editable2' } );
+view.change( writer => {
+	writer.insert( Position.createAt( editable1 ), writer.createText( 'First editable.' ) );
+	writer.insert( Position.createAt( editable2 ), writer.createText( 'Second editable.' ) );
+
+	writer.setSelection( editable1 );
+} );
 
 editable1.on( 'change:isFocused', () => {
 	domEditable1.style.backgroundColor = editable1.isFocused ? 'green' : 'red';
 } );
+
 editable2.on( 'change:isFocused', () => {
 	domEditable2.style.backgroundColor = editable2.isFocused ? 'green' : 'red';
 } );
 
-viewDocument.focus();
+view.focus();

+ 16 - 6
packages/ckeditor5-engine/tests/view/manual/immutable.js

@@ -5,16 +5,26 @@
 
 /* globals document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/view';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
-viewDocument.createRoot( document.getElementById( 'editor' ) );
+const view = new View();
+const viewDocument = view.document;
+createViewRoot( viewDocument, 'div' );
+view.attachDomRoot( document.getElementById( 'editor' ) );
 
-setData( viewDocument,
+setData( view,
 	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' +
 	'<container:p></container:p>' +
 	'<container:p><attribute:strong></attribute:strong></container:p>' +
-	'<container:p>bom</container:p>' );
+	'<container:p>bom</container:p>'
+);
 
-viewDocument.render();
+viewDocument.on( 'selectionChange', () => {
+	// Re-render view selection each time selection is changed.
+	// See https://github.com/ckeditor/ckeditor5-engine/issues/796.
+	view.render();
+} );
+
+view.focus();

+ 16 - 11
packages/ckeditor5-engine/tests/view/manual/inline-filler.js

@@ -5,22 +5,27 @@
 
 /* globals document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
+import createViewRoot from '../_utils/createroot';
 import { setData } from '../../../src/dev-utils/view';
 
-const viewDocument = new Document();
-viewDocument.createRoot( document.getElementById( 'editor' ) );
+const view = new View();
+const viewDocument = view.document;
+createViewRoot( viewDocument );
+view.attachDomRoot( document.getElementById( 'editor' ) );
 
-viewDocument.isFocused = true;
+setData(
+	view,
+	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>'
+);
 
-setData( viewDocument,
-	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' );
+view.focus();
 
 viewDocument.on( 'selectionChange', ( evt, data ) => {
-	viewDocument.selection.setTo( data.newSelection );
-
-	// Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/796.
-	viewDocument.render();
+	view.change( writer => {
+		// Re-render view selection each time selection is changed.
+		// See https://github.com/ckeditor/ckeditor5-engine/issues/796.
+		writer.setSelection( data.newSelection );
+	} );
 } );
 
-viewDocument.render();

+ 15 - 6
packages/ckeditor5-engine/tests/view/manual/keyobserver.js

@@ -5,15 +5,24 @@
 
 /* globals console, document */
 
-import Document from '../../../src/view/document';
-import { setData } from '../../../src/dev-utils/view';
+import View from '../../../src/view/view';
+import Position from '../../../src/view/position';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
+const view = new View();
+const viewDocument = view.document;
 
 viewDocument.on( 'keydown', ( evt, data ) => console.log( 'keydown', data ) );
 viewDocument.on( 'keyup', ( evt, data ) => console.log( 'keyup', data ) );
 
-viewDocument.createRoot( document.getElementById( 'editable' ), 'editable' );
-setData( viewDocument, 'foo{}bar', { rootName: 'editable' } );
-viewDocument.focus();
+const viewRoot = createViewRoot( viewDocument, 'div', 'editable' );
+view.attachDomRoot( document.getElementById( 'editable' ), 'editable' );
+
+view.change( writer => {
+	const text = writer.createText( 'foobar' );
+	writer.insert( Position.createAt( viewRoot ), text );
+	writer.setSelection( text, 3 );
+} );
+
+view.focus();
 

+ 10 - 8
packages/ckeditor5-engine/tests/view/manual/mutationobserver.js

@@ -5,19 +5,21 @@
 
 /* globals console, document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
+import createViewRoot from '../_utils/createroot';
 import { setData } from '../../../src/dev-utils/view';
 
-const viewDocument = new Document();
-viewDocument.createRoot( document.getElementById( 'editor' ) );
+const view = new View();
+const viewDocument = view.document;
+createViewRoot( viewDocument );
+view.attachDomRoot( document.getElementById( 'editor' ) );
 
 viewDocument.on( 'mutations', ( evt, mutations ) => console.log( mutations ) );
 viewDocument.on( 'selectionChange', ( evt, data ) => {
-	viewDocument.selection.setTo( data.newSelection );
+	view.change( writer => writer.setSelection( data.newSelection ) );
 } );
 
-setData( viewDocument,
+setData( view,
 	'<container:p>foo</container:p>' +
-	'<container:p>bar</container:p>' );
-
-viewDocument.render();
+	'<container:p>bar</container:p>'
+);

+ 7 - 6
packages/ckeditor5-engine/tests/view/manual/noselection-iframe.js

@@ -5,15 +5,16 @@
 
 /* globals document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/view';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
+const view = new View();
+const viewDocument = view.document;
+createViewRoot( viewDocument );
 const iframe = document.getElementById( 'iframe' );
-viewDocument.createRoot( iframe.contentWindow.document.getElementById( 'editor' ) );
+view.attachDomRoot( iframe.contentWindow.document.getElementById( 'editor' ) );
 
-setData( viewDocument,
+setData( view,
 	'<container:p>foo</container:p>' +
 	'<container:p>bar</container:p>' );
-
-viewDocument.render();

+ 13 - 6
packages/ckeditor5-engine/tests/view/manual/noselection.js

@@ -5,14 +5,21 @@
 
 /* globals document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/view';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
-viewDocument.createRoot( document.getElementById( 'editor' ) );
+const view = new View();
+const viewDocument = view.document;
+createViewRoot( viewDocument );
+view.attachDomRoot( document.getElementById( 'editor' ) );
 
-setData( viewDocument,
+viewDocument.on( 'selectionChange', () => {
+	// Re-render view selection each time selection is changed.
+	// See https://github.com/ckeditor/ckeditor5-engine/issues/796.
+	view.render();
+} );
+
+setData( view,
 	'<container:p>foo</container:p>' +
 	'<container:p>bar</container:p>' );
-
-viewDocument.render();

+ 9 - 8
packages/ckeditor5-engine/tests/view/manual/selectionobserver.js

@@ -5,24 +5,25 @@
 
 /* globals console, document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/view';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
-viewDocument.createRoot( document.getElementById( 'editor' ) );
+const view = new View();
+const viewDocument = view.document;
+createViewRoot( viewDocument );
+view.attachDomRoot( document.getElementById( 'editor' ) );
 
-setData( viewDocument,
+setData( view,
 	'<container:p><attribute:b>foo</attribute:b>bar</container:p>' +
 	'<container:p>bom</container:p>' );
 
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 	console.log( 'selectionChange', data );
-	viewDocument.selection.setTo( data.newSelection );
+	view.change( writer => writer.setSelection( data.newSelection ) );
 } );
 
 viewDocument.on( 'selectionChangeDone', ( evt, data ) => {
 	console.log( '%c selectionChangeDone ', 'background: #222; color: #bada55', data );
-	viewDocument.selection.setTo( data.newSelection );
+	view.change( writer => writer.setSelection( data.newSelection ) );
 } );
-
-viewDocument.render();

+ 15 - 20
packages/ckeditor5-engine/tests/view/manual/uielement.js

@@ -13,34 +13,28 @@ 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 Undo from '@ckeditor/ckeditor5-undo/src/undo';
-import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
-import writer from '../../../src/view/writer';
 
-function createEndingUIElement() {
-	const element = new UIElement( 'span' );
-
-	element.render = function( domDocument ) {
+function createEndingUIElement( writer ) {
+	const element = writer.createUIElement( 'span', null, function( domDocument ) {
 		const root = this.toDomElement( domDocument );
 		root.classList.add( 'ui-element' );
 		root.innerHTML = 'END OF PARAGRAPH';
 
 		return root;
-	};
+	} );
 
 	return element;
 }
 
-function createMiddleUIElement() {
-	const element = new UIElement( 'span' );
-
-	element.render = function( domDocument ) {
+function createMiddleUIElement( writer ) {
+	const element = writer.createUIElement( 'span', null, function( domDocument ) {
 		const root = this.toDomElement( domDocument );
 		root.classList.add( 'ui-element' );
 		root.innerHTML = 'X';
 
 		return root;
-	};
+	} );
 
 	return element;
 }
@@ -53,7 +47,7 @@ class UIElementTestPlugin extends Plugin {
 		// Add some UIElement to each paragraph.
 		editing.downcastDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
 			const viewP = conversionApi.mapper.toViewElement( data.item );
-			viewP.appendChildren( createEndingUIElement() );
+			viewP.appendChildren( createEndingUIElement( conversionApi.writer ) );
 		}, { priority: 'lowest' } );
 	}
 }
@@ -65,18 +59,19 @@ ClassicEditor
 	} )
 	.then( editor => {
 		window.editor = editor;
+		const view = editor.editing.view;
 
 		// Add some UI elements.
-		const viewRoot = editor.editing.view.getRoot();
+		const viewRoot = editor.editing.view.document.getRoot();
 		const viewText1 = viewRoot.getChild( 0 ).getChild( 0 );
 		const viewText2 = viewRoot.getChild( 1 ).getChild( 0 );
 
-		writer.insert( new Position( viewText1, 20 ), createMiddleUIElement() );
-		writer.insert( new Position( viewText1, 20 ), createMiddleUIElement() );
-		writer.insert( new Position( viewText2, 0 ), createMiddleUIElement() );
-		writer.insert( new Position( viewText2, 6 ), createMiddleUIElement() );
-
-		editor.editing.view.render();
+		view.change( writer => {
+			writer.insert( new Position( viewText1, 20 ), createMiddleUIElement( writer ) );
+			writer.insert( new Position( viewText1, 20 ), createMiddleUIElement( writer ) );
+			writer.insert( new Position( viewText2, 0 ), createMiddleUIElement( writer ) );
+			writer.insert( new Position( viewText2, 6 ), createMiddleUIElement( writer ) );
+		} );
 	} )
 	.catch( err => {
 		console.error( err.stack );

+ 11 - 9
packages/ckeditor5-engine/tests/view/manual/x-index.js

@@ -5,24 +5,26 @@
 
 /* globals console, document */
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/view';
+import createViewRoot from '../_utils/createroot';
 
-const viewDocument = new Document();
-viewDocument.createRoot( document.getElementById( 'editor' ) );
+const view = new View();
+const viewDocument = view.document;
+createViewRoot( viewDocument );
+view.attachDomRoot( document.getElementById( 'editor' ) );
 
-viewDocument.isFocused = true;
-
-setData( viewDocument,
+setData( view,
 	'<container:p>fo{}o</container:p>' +
 	'<container:p></container:p>' +
 	'<container:p><attribute:strong></attribute:strong></container:p>' +
 	'<container:p>bar</container:p>' );
 
+view.focus();
+
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 	const node = data.newSelection.getFirstPosition().parent;
 	console.log( node.name ? node.name : node._data );
-	viewDocument.selection.setTo( data.newSelection );
-} );
 
-viewDocument.render();
+	view.change( writer => writer.setSelection( data.newSelection ) );
+} );

+ 5 - 5
packages/ckeditor5-engine/tests/view/matcher.js

@@ -306,7 +306,7 @@ describe( 'Matcher', () => {
 			};
 			const matcher = new Matcher( pattern );
 			const el = new Element( 'a' );
-			el.addClass( 'foo', 'bar', 'baz' );
+			el._addClass( [ 'foo', 'bar', 'baz' ] );
 
 			const result = matcher.match( el );
 			expect( result ).to.be.an( 'object' );
@@ -328,7 +328,7 @@ describe( 'Matcher', () => {
 			};
 			const matcher = new Matcher( pattern );
 			const el = new Element( 'a' );
-			el.setStyle( {
+			el._setStyle( {
 				color: 'red',
 				position: 'relative'
 			} );
@@ -376,9 +376,9 @@ describe( 'Matcher', () => {
 			const el2 = new Element( 'p' );
 			const el3 = new Element( 'p' );
 
-			el1.addClass( 'red-foreground' );
-			el2.addClass( 'red-background' );
-			el3.addClass( 'blue-text' );
+			el1._addClass( 'red-foreground' );
+			el2._addClass( 'red-background' );
+			el3._addClass( 'blue-text' );
 
 			const result = matcher.matchAll( el1, el2, el3 );
 			expect( result ).to.be.an( 'array' );

+ 9 - 10
packages/ckeditor5-engine/tests/view/node.js

@@ -222,7 +222,7 @@ describe( 'Node', () => {
 		it( 'should return Document attached to the parent element', () => {
 			const docMock = createDocumentMock();
 			const parent = new RootEditableElement( 'div' );
-			parent.document = docMock;
+			parent._document = docMock;
 			const child = new Element( 'p' );
 
 			child.parent = parent;
@@ -248,7 +248,7 @@ describe( 'Node', () => {
 
 		it( 'should return root element', () => {
 			const parent = new RootEditableElement( 'div' );
-			parent.document = createDocumentMock();
+			parent._document = createDocumentMock();
 			const child = new Element( 'p' );
 
 			child.parent = parent;
@@ -303,8 +303,7 @@ describe( 'Node', () => {
 
 		beforeEach( () => {
 			text = new Text( 'foo' );
-			img = new Element( 'img' );
-			img.setAttribute( 'src', 'img.png' );
+			img = new Element( 'img', { 'src': 'img.png' } );
 
 			root = new Element( 'p', { renderer: { markToSync: rootChangeSpy } } );
 			root.appendChildren( [ text, img ] );
@@ -323,31 +322,31 @@ describe( 'Node', () => {
 				imgChangeSpy( 'attributes', node );
 			} );
 
-			img.setAttribute( 'width', 100 );
+			img._setAttribute( 'width', 100 );
 
 			sinon.assert.calledOnce( imgChangeSpy );
 			sinon.assert.calledWith( imgChangeSpy, 'attributes', img );
 		} );
 
 		it( 'should be fired on the parent', () => {
-			img.setAttribute( 'width', 100 );
+			img._setAttribute( 'width', 100 );
 
 			sinon.assert.calledOnce( rootChangeSpy );
 			sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 		} );
 
-		describe( 'setAttribute()', () => {
+		describe( '_setAttribute()', () => {
 			it( 'should fire change event', () => {
-				img.setAttribute( 'width', 100 );
+				img._setAttribute( 'width', 100 );
 
 				sinon.assert.calledOnce( rootChangeSpy );
 				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 			} );
 		} );
 
-		describe( 'removeAttribute()', () => {
+		describe( '_removeAttribute()', () => {
 			it( 'should fire change event', () => {
-				img.removeAttribute( 'src' );
+				img._removeAttribute( 'src' );
 
 				sinon.assert.calledOnce( rootChangeSpy );
 				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );

+ 6 - 5
packages/ckeditor5-engine/tests/view/observer/clickobserver.js

@@ -6,18 +6,19 @@
 /* globals document */
 
 import ClickObserver from '../../../src/view/observer/clickobserver';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 
 describe( 'ClickObserver', () => {
-	let viewDocument, observer;
+	let view, viewDocument, observer;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
-		observer = viewDocument.addObserver( ClickObserver );
+		view = new View();
+		viewDocument = view.document;
+		observer = view.addObserver( ClickObserver );
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	it( 'should define domEventType', () => {

+ 9 - 7
packages/ckeditor5-engine/tests/view/observer/domeventdata.js

@@ -6,31 +6,33 @@
 /* globals document */
 
 import DomEventData from '../../../src/view/observer/domeventdata';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 
 describe( 'DomEventData', () => {
-	let viewDocument, viewBody, domRoot;
+	let view, viewDocument, viewBody, domRoot;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 
 		domRoot = document.createElement( 'div' );
 		domRoot.innerHTML = '<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>';
 		document.body.appendChild( domRoot );
 
-		viewBody = viewDocument.domConverter.domToView( document.body, { bind: true } );
+		viewBody = view.domConverter.domToView( document.body, { bind: true } );
 	} );
 
 	afterEach( () => {
 		domRoot.parentElement.removeChild( domRoot );
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	describe( 'constructor()', () => {
 		it( 'sets properties', () => {
 			const domEvt = { target: document.body };
-			const data = new DomEventData( viewDocument, domEvt, { foo: 1, bar: true } );
+			const data = new DomEventData( view, domEvt, { foo: 1, bar: true } );
 
+			expect( data ).to.have.property( 'view', view );
 			expect( data ).to.have.property( 'document', viewDocument );
 			expect( data ).to.have.property( 'domEvent', domEvt );
 			expect( data ).to.have.property( 'domTarget', document.body );
@@ -43,7 +45,7 @@ describe( 'DomEventData', () => {
 	describe( 'target', () => {
 		it( 'returns bound element', () => {
 			const domEvt = { target: document.body };
-			const data = new DomEventData( viewDocument, domEvt );
+			const data = new DomEventData( view, domEvt );
 
 			expect( data ).to.have.property( 'target', viewBody );
 		} );

+ 25 - 24
packages/ckeditor5-engine/tests/view/observer/domeventobserver.js

@@ -7,13 +7,13 @@
 
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import Observer from '../../../src/view/observer/observer';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import UIElement from '../../../src/view/uielement';
 import createViewRoot from '../_utils/createroot';
 
 class ClickObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		this.domEventType = 'click';
 	}
@@ -24,8 +24,8 @@ class ClickObserver extends DomEventObserver {
 }
 
 class MultiObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		this.domEventType = [ 'evt1', 'evt2' ];
 	}
@@ -36,22 +36,23 @@ class MultiObserver extends DomEventObserver {
 }
 
 class ClickCapturingObserver extends ClickObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 		this.useCapture = true;
 	}
 }
 
 describe( 'DomEventObserver', () => {
-	let viewDocument;
+	let view, viewDocument;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	describe( 'constructor()', () => {
@@ -68,8 +69,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		viewDocument.addObserver( ClickObserver );
+		view.attachDomRoot( domElement );
+		view.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 
 		domElement.dispatchEvent( domEvent );
@@ -91,8 +92,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy2 = sinon.spy();
 
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		viewDocument.addObserver( MultiObserver );
+		view.attachDomRoot( domElement );
+		view.addObserver( MultiObserver );
 		viewDocument.on( 'evt1', evtSpy1 );
 		viewDocument.on( 'evt2', evtSpy2 );
 
@@ -109,8 +110,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		const testObserver = viewDocument.addObserver( ClickObserver );
+		view.attachDomRoot( domElement );
+		const testObserver = view.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 
 		testObserver.disable();
@@ -126,8 +127,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		const testObserver = viewDocument.addObserver( ClickObserver );
+		view.attachDomRoot( domElement );
+		const testObserver = view.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 
 		testObserver.disable();
@@ -149,8 +150,8 @@ describe( 'DomEventObserver', () => {
 		const domEvent = new MouseEvent( 'click' );
 		domElement.appendChild( childDomElement );
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		viewDocument.addObserver( ClickCapturingObserver );
+		view.attachDomRoot( domElement );
+		view.addObserver( ClickCapturingObserver );
 
 		viewDocument.on( 'click', ( evt, domEventData ) => {
 			expect( domEventData.domEvent.eventPhase ).to.equal( domEventData.domEvent.CAPTURING_PHASE );
@@ -179,14 +180,14 @@ describe( 'DomEventObserver', () => {
 		beforeEach( () => {
 			domRoot = document.createElement( 'div' );
 			const viewRoot = createViewRoot( viewDocument );
-			viewDocument.attachDomRoot( domRoot );
+			view.attachDomRoot( domRoot );
 			uiElement = createUIElement( 'p' );
 			viewRoot.appendChildren( uiElement );
-			viewDocument.render();
+			view.render();
 
 			domEvent = new MouseEvent( 'click', { bubbles: true } );
 			evtSpy = sinon.spy();
-			viewDocument.addObserver( ClickObserver );
+			view.addObserver( ClickObserver );
 			viewDocument.on( 'click', evtSpy );
 		} );
 
@@ -213,7 +214,7 @@ describe( 'DomEventObserver', () => {
 
 	describe( 'fire', () => {
 		it( 'should do nothing if observer is disabled', () => {
-			const testObserver = new ClickObserver( viewDocument );
+			const testObserver = new ClickObserver( view );
 			const fireSpy = sinon.spy( viewDocument, 'fire' );
 
 			testObserver.disable();

+ 11 - 10
packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js

@@ -7,14 +7,14 @@
 
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import DomEventData from '../../../src/view/observer/domeventdata';
 import createViewRoot from '../_utils/createroot';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData, stringify } from '../../../src/dev-utils/view';
 
 describe( 'FakeSelectionObserver', () => {
-	let observer, viewDocument, root, domRoot;
+	let observer, view, viewDocument, root, domRoot;
 
 	before( () => {
 		domRoot = createElement( document, 'div', {
@@ -28,19 +28,20 @@ describe( 'FakeSelectionObserver', () => {
 	} );
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		root = createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domRoot );
-		observer = viewDocument.getObserver( FakeSelectionObserver );
-		viewDocument.selection.setFake();
+		view.attachDomRoot( domRoot );
+		observer = view.getObserver( FakeSelectionObserver );
+		viewDocument.selection._setFake();
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	it( 'should do nothing if selection is not fake', () => {
-		viewDocument.selection.setFake( false );
+		viewDocument.selection._setFake( false );
 
 		return checkEventPrevention( keyCodes.arrowleft, false );
 	} );
@@ -190,7 +191,7 @@ describe( 'FakeSelectionObserver', () => {
 				resolve();
 			} );
 
-			setData( viewDocument, initialData );
+			setData( view, initialData );
 			changeFakeSelectionPressing( keyCode );
 		} );
 	}
@@ -199,7 +200,7 @@ describe( 'FakeSelectionObserver', () => {
 	//
 	// @param {Number} keyCode
 	function changeFakeSelectionPressing( keyCode ) {
-		viewDocument.selection.setFake();
+		viewDocument.selection._setFake();
 
 		const data = {
 			keyCode,

+ 28 - 23
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -5,21 +5,21 @@
 
 /* globals document */
 import FocusObserver from '../../../src/view/observer/focusobserver';
-import ViewDocument from '../../../src/view/document';
-import ViewRange from '../../../src/view/range';
+import View from '../../../src/view/view';
 import createViewRoot from '../_utils/createroot';
 import { setData } from '../../../src/dev-utils/view';
 
 describe( 'FocusObserver', () => {
-	let viewDocument, observer;
+	let view, viewDocument, observer;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
-		observer = viewDocument.getObserver( FocusObserver );
+		view = new View();
+		viewDocument = view.document;
+		observer = view.getObserver( FocusObserver );
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	it( 'should define domEventType', () => {
@@ -58,7 +58,7 @@ describe( 'FocusObserver', () => {
 		} );
 
 		it( 'should render document after blurring', () => {
-			const renderSpy = sinon.spy( viewDocument, 'render' );
+			const renderSpy = sinon.spy( view, 'render' );
 
 			observer.onDomEvent( { type: 'blur', target: document.body } );
 
@@ -74,7 +74,7 @@ describe( 'FocusObserver', () => {
 			domHeader = document.createElement( 'h1' );
 
 			viewMain = createViewRoot( viewDocument );
-			viewDocument.attachDomRoot( domMain );
+			view.attachDomRoot( domMain );
 		} );
 
 		it( 'should set isFocused to true on focus', () => {
@@ -94,7 +94,9 @@ describe( 'FocusObserver', () => {
 		} );
 
 		it( 'should set isFocused to false on blur when selection in same editable', () => {
-			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+			view.change( writer => {
+				writer.setSelection( viewMain, 0 );
+			} );
 
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
@@ -106,7 +108,9 @@ describe( 'FocusObserver', () => {
 		} );
 
 		it( 'should not set isFocused to false on blur when it is fired on other editable', () => {
-			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+			view.change( writer => {
+				writer.setSelection( viewMain, 0 );
+			} );
 
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
@@ -118,7 +122,7 @@ describe( 'FocusObserver', () => {
 		} );
 
 		it( 'should delay rendering by 50ms', () => {
-			const renderSpy = sinon.spy( viewDocument, 'render' );
+			const renderSpy = sinon.spy( view, 'render' );
 			const clock = sinon.useFakeTimers();
 
 			observer.onDomEvent( { type: 'focus', target: domMain } );
@@ -130,7 +134,7 @@ describe( 'FocusObserver', () => {
 		} );
 
 		it( 'should not call render if destroyed', () => {
-			const renderSpy = sinon.spy( viewDocument, 'render' );
+			const renderSpy = sinon.spy( view, 'render' );
 			const clock = sinon.useFakeTimers();
 
 			observer.onDomEvent( { type: 'focus', target: domMain } );
@@ -150,24 +154,25 @@ describe( 'FocusObserver', () => {
 			domRoot = document.createElement( 'div' );
 			document.body.appendChild( domRoot );
 
-			viewDocument = new ViewDocument();
+			view = new View();
+			viewDocument = view.document;
 			createViewRoot( viewDocument );
-			viewDocument.attachDomRoot( domRoot );
+			view.attachDomRoot( domRoot );
 
-			observer = viewDocument.getObserver( FocusObserver );
+			observer = view.getObserver( FocusObserver );
 		} );
 
 		it( 'should always render document after selectionChange event', done => {
 			const selectionChangeSpy = sinon.spy();
 			const renderSpy = sinon.spy();
 
-			setData( viewDocument, '<div contenteditable="true">foo bar</div>' );
-			viewDocument.render();
+			setData( view, '<div contenteditable="true">foo bar</div>' );
+			view.render();
 
 			viewDocument.on( 'selectionChange', selectionChangeSpy );
-			viewDocument.on( 'render', renderSpy, { priority: 'low' } );
+			view.on( 'render', renderSpy, { priority: 'low' } );
 
-			viewDocument.on( 'render', () => {
+			view.on( 'render', () => {
 				sinon.assert.callOrder( selectionChangeSpy, renderSpy );
 				done();
 			}, { priority: 'low' } );
@@ -182,14 +187,14 @@ describe( 'FocusObserver', () => {
 			const selectionChangeSpy = sinon.spy();
 			const renderSpy = sinon.spy();
 
-			setData( viewDocument, '<div contenteditable="true">foo bar</div>' );
-			viewDocument.render();
+			setData( view, '<div contenteditable="true">foo bar</div>' );
+			view.render();
 			const domEditable = domRoot.childNodes[ 0 ];
 
 			viewDocument.on( 'selectionChange', selectionChangeSpy );
-			viewDocument.on( 'render', renderSpy, { priority: 'low' } );
+			view.on( 'render', renderSpy, { priority: 'low' } );
 
-			viewDocument.on( 'render', () => {
+			view.on( 'render', () => {
 				sinon.assert.notCalled( selectionChangeSpy );
 				sinon.assert.called( renderSpy );
 

+ 6 - 5
packages/ckeditor5-engine/tests/view/observer/keyobserver.js

@@ -6,19 +6,20 @@
 /* globals document */
 
 import KeyObserver from '../../../src/view/observer/keyobserver';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 describe( 'KeyObserver', () => {
-	let viewDocument, observer;
+	let view, viewDocument, observer;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
-		observer = viewDocument.getObserver( KeyObserver );
+		view = new View();
+		viewDocument = view.document;
+		observer = view.getObserver( KeyObserver );
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	it( 'should define domEventType', () => {

+ 6 - 5
packages/ckeditor5-engine/tests/view/observer/mouseobserver.js

@@ -6,18 +6,19 @@
 /* globals document */
 
 import MouseObserver from '../../../src/view/observer/mouseobserver';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 
 describe( 'MouseObserver', () => {
-	let viewDocument, observer;
+	let view, viewDocument, observer;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
-		observer = viewDocument.addObserver( MouseObserver );
+		view = new View();
+		viewDocument = view.document;
+		observer = view.addObserver( MouseObserver );
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	it( 'should define domEventType', () => {

+ 36 - 34
packages/ckeditor5-engine/tests/view/observer/mutationobserver.js

@@ -5,30 +5,31 @@
 
 /* globals document */
 
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import MutationObserver from '../../../src/view/observer/mutationobserver';
 import UIElement from '../../../src/view/uielement';
 import createViewRoot from '../_utils/createroot';
 import { parse } from '../../../src/dev-utils/view';
 
 describe( 'MutationObserver', () => {
-	let domEditor, viewDocument, viewRoot, mutationObserver, lastMutations, domRoot;
+	let view, domEditor, viewDocument, viewRoot, mutationObserver, lastMutations, domRoot;
 
 	beforeEach( () => {
 		domRoot = document.createElement( 'div' );
 		domRoot.innerHTML = '<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>';
 		document.body.appendChild( domRoot );
 
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		domEditor = document.getElementById( 'main' );
 		lastMutations = null;
 
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domEditor );
-		viewDocument.selection.setTo( null );
+		view.attachDomRoot( domEditor );
+		viewDocument.selection._setTo( null );
 		document.getSelection().removeAllRanges();
 
-		mutationObserver = viewDocument.getObserver( MutationObserver );
+		mutationObserver = view.getObserver( MutationObserver );
 
 		viewDocument.on( 'mutations', ( evt, mutations ) => {
 			lastMutations = mutations;
@@ -38,14 +39,14 @@ describe( 'MutationObserver', () => {
 
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 	} );
 
 	afterEach( () => {
 		mutationObserver.disable();
 
 		domRoot.parentElement.removeChild( domRoot );
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	it( 'should handle typing', () => {
@@ -65,7 +66,7 @@ describe( 'MutationObserver', () => {
 		const additional = document.getElementById( 'additional' );
 		mutationObserver.disable();
 		createViewRoot( viewDocument, 'div', 'additional' );
-		viewDocument.attachDomRoot( additional, 'additional' );
+		view.attachDomRoot( additional, 'additional' );
 
 		additional.textContent = 'foobar';
 		mutationObserver.flush();
@@ -97,7 +98,7 @@ describe( 'MutationObserver', () => {
 	it( 'should handle unbold', () => {
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 		viewRoot.appendChildren( parse( '<container:p><attribute:b>foo</attribute:b></container:p>' ) );
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 0 ];
 		const domB = domP.childNodes[ 0 ];
@@ -200,13 +201,13 @@ describe( 'MutationObserver', () => {
 
 		// Prepare AdditionalEditor
 		createViewRoot( viewDocument, 'div', 'additional' );
-		viewDocument.attachDomRoot( domAdditionalEditor, 'additional' );
+		view.attachDomRoot( domAdditionalEditor, 'additional' );
 
 		viewDocument.getRoot( 'additional' ).appendChildren(
 			parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 
 		// Render AdditionalEditor (first editor has been rendered in the beforeEach function)
-		viewDocument.render();
+		view.render();
 
 		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
 		domAdditionalEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
@@ -224,12 +225,12 @@ describe( 'MutationObserver', () => {
 	} );
 
 	it( 'should fire children mutation if the mutation occurred in the inline filler', () => {
-		const { view, selection } = parse( '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+		const { view: viewContainer, selection } = parse( '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
 
-		viewRoot.appendChildren( view );
-		viewDocument.selection.setTo( selection );
-
-		viewDocument.render();
+		view.change( writer => {
+			viewRoot.appendChildren( viewContainer );
+			writer.setSelection( selection );
+		} );
 
 		const inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
 		inlineFiller.data += 'x';
@@ -242,19 +243,20 @@ describe( 'MutationObserver', () => {
 	} );
 
 	it( 'should have no inline filler in mutation', () => {
-		const { view, selection } = parse( '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+		const { view: viewContainer, selection } = parse( '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
 
-		viewRoot.appendChildren( view );
-		viewDocument.selection.setTo( selection );
-
-		viewDocument.render();
+		view.change( writer => {
+			viewRoot.appendChildren( viewContainer );
+			writer.setSelection( selection );
+		} );
 
 		let inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
 		inlineFiller.data += 'x';
 
-		view.getChild( 1 ).appendChildren( parse( 'x' ) );
-		mutationObserver.flush();
-		viewDocument.render();
+		view.change( () => {
+			viewContainer.getChild( 1 ).appendChildren( parse( 'x' ) );
+			mutationObserver.flush();
+		} );
 
 		inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
 		inlineFiller.data += 'y';
@@ -270,7 +272,7 @@ describe( 'MutationObserver', () => {
 	it( 'should have no block filler in mutation', () => {
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		domP.removeChild( domP.childNodes[ 0 ] );
@@ -289,7 +291,7 @@ describe( 'MutationObserver', () => {
 	it( 'should ignore mutation with bogus br inserted on the end of the empty paragraph', () => {
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'br' ) );
@@ -302,7 +304,7 @@ describe( 'MutationObserver', () => {
 	it( 'should ignore mutation with bogus br inserted on the end of the paragraph with text', () => {
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'br' ) );
@@ -315,7 +317,7 @@ describe( 'MutationObserver', () => {
 	it( 'should ignore mutation with bogus br inserted on the end of the paragraph while processing text mutations', () => {
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		domP.childNodes[ 0 ].data = 'foo ';
@@ -332,7 +334,7 @@ describe( 'MutationObserver', () => {
 	it( 'should ignore child mutations which resulted in no changes – when element contains elements', () => {
 		viewRoot.appendChildren( parse( '<container:p><container:x></container:x></container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domY = document.createElement( 'y' );
@@ -366,7 +368,7 @@ describe( 'MutationObserver', () => {
 	it( 'should not ignore mutation with br inserted not on the end of the paragraph', () => {
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		domP.insertBefore( document.createElement( 'br' ), domP.childNodes[ 0 ] );
@@ -385,7 +387,7 @@ describe( 'MutationObserver', () => {
 	it( 'should not ignore mutation inserting element different than br on the end of the empty paragraph', () => {
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'span' ) );
@@ -403,7 +405,7 @@ describe( 'MutationObserver', () => {
 	it( 'should not ignore mutation inserting element different than br on the end of the paragraph with text', () => {
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
-		viewDocument.render();
+		view.render();
 
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'span' ) );
@@ -437,7 +439,7 @@ describe( 'MutationObserver', () => {
 			const uiElement = createUIElement( 'div' );
 			viewRoot.appendChildren( uiElement );
 
-			viewDocument.render();
+			view.render();
 		} );
 
 		it( 'should not collect text mutations from UIElement', () => {

+ 4 - 3
packages/ckeditor5-engine/tests/view/observer/observer.js

@@ -4,15 +4,16 @@
  */
 
 import Observer from '../../../src/view/observer/observer';
+import View from '../../../src/view/view';
 
 describe( 'Observer', () => {
 	describe( 'constructor()', () => {
 		it( 'should create Observer with properties', () => {
-			const viewDocument = {};
-			const observer = new Observer( viewDocument );
+			const view = new View();
+			const observer = new Observer( view );
 
 			expect( observer ).to.be.an.instanceof( Observer );
-			expect( observer ).to.have.property( 'document' ).that.equals( viewDocument );
+			expect( observer ).to.have.property( 'document' ).that.equals( view.document );
 			expect( observer ).to.have.property( 'isEnabled' ).that.is.false;
 		} );
 	} );

+ 37 - 34
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -8,7 +8,7 @@
 import ViewRange from '../../../src/view/range';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import ViewSelection from '../../../src/view/selection';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import SelectionObserver from '../../../src/view/observer/selectionobserver';
 import FocusObserver from '../../../src/view/observer/focusobserver';
 import log from '@ckeditor/ckeditor5-utils/src/log';
@@ -18,7 +18,7 @@ import { parse } from '../../../src/dev-utils/view';
 testUtils.createSinonSandbox();
 
 describe( 'SelectionObserver', () => {
-	let viewDocument, viewRoot, selectionObserver, domRoot, domMain, domDocument;
+	let view, viewDocument, viewRoot, selectionObserver, domRoot, domMain, domDocument;
 
 	beforeEach( done => {
 		domDocument = document;
@@ -27,25 +27,26 @@ describe( 'SelectionObserver', () => {
 		domMain = domRoot.childNodes[ 0 ];
 		domDocument.body.appendChild( domRoot );
 
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domMain );
+		view.attachDomRoot( domMain );
 
-		selectionObserver = viewDocument.getObserver( SelectionObserver );
+		selectionObserver = view.getObserver( SelectionObserver );
 
 		viewRoot = viewDocument.getRoot();
 
-		viewRoot.appendChildren( parse(
-			'<container:p>xxx<ui:span></ui:span></container:p>' +
-			'<container:p>yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</container:p>' ) );
+		view.change( writer => {
+			viewRoot.appendChildren( parse(
+				'<container:p>xxx<ui:span></ui:span></container:p>' +
+				'<container:p>yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</container:p>' ) );
 
-		viewDocument.render();
+			writer.setSelection( null );
+			domDocument.getSelection().removeAllRanges();
 
-		viewDocument.selection.setTo( null );
-		domDocument.getSelection().removeAllRanges();
-
-		viewDocument.isFocused = true;
-		domMain.focus();
+			viewDocument.isFocused = true;
+			domMain.focus();
+		} );
 
 		selectionObserver.enable();
 
@@ -56,7 +57,7 @@ describe( 'SelectionObserver', () => {
 	afterEach( () => {
 		domRoot.parentElement.removeChild( domRoot );
 
-		viewDocument.destroy();
+		view.destroy();
 	} );
 
 	it( 'should fire selectionChange when it is the only change', done => {
@@ -86,7 +87,7 @@ describe( 'SelectionObserver', () => {
 	it( 'should add only one listener to one document', done => {
 		// Add second roots to ensure that listener is added once.
 		createViewRoot( viewDocument, 'div', 'additional' );
-		viewDocument.attachDomRoot( domDocument.getElementById( 'additional' ), 'additional' );
+		view.attachDomRoot( domDocument.getElementById( 'additional' ), 'additional' );
 
 		viewDocument.on( 'selectionChange', () => {
 			done();
@@ -103,12 +104,14 @@ describe( 'SelectionObserver', () => {
 		setTimeout( done, 70 );
 
 		const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
-		viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
-		viewDocument.render();
+
+		view.change( writer => {
+			writer.setSelection( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
+		} );
 	} );
 
 	it( 'should not fired if observer is disabled', done => {
-		viewDocument.getObserver( SelectionObserver ).disable();
+		view.getObserver( SelectionObserver ).disable();
 
 		viewDocument.on( 'selectionChange', () => {
 			throw 'selectionChange on render';
@@ -162,7 +165,9 @@ describe( 'SelectionObserver', () => {
 		let counter = 70;
 
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
-		viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
+		view.change( writer => {
+			writer.setSelection( viewFoo, 0 );
+		} );
 
 		return new Promise( ( resolve, reject ) => {
 			testUtils.sinon.stub( log, 'warn' ).callsFake( msg => {
@@ -186,7 +191,7 @@ describe( 'SelectionObserver', () => {
 
 	it( 'should not be treated as an infinite loop if selection is changed only few times', done => {
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
-		viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
+		viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
 		const spy = testUtils.sinon.spy( log, 'warn' );
 
 		viewDocument.on( 'selectionChangeDone', () => {
@@ -208,8 +213,8 @@ describe( 'SelectionObserver', () => {
 		// We need to recreate SelectionObserver, so it will use mocked setInterval.
 		selectionObserver.disable();
 		selectionObserver.destroy();
-		viewDocument._observers.delete( SelectionObserver );
-		viewDocument.addObserver( SelectionObserver );
+		view._observers.delete( SelectionObserver );
+		view.addObserver( SelectionObserver );
 
 		return doChanges()
 			.then( doChanges )
@@ -238,7 +243,7 @@ describe( 'SelectionObserver', () => {
 		viewDocument.on( 'selectionChangeDone', spy );
 
 		// Disable focus observer to not re-render view on each focus.
-		viewDocument.getObserver( FocusObserver ).disable();
+		view.getObserver( FocusObserver ).disable();
 
 		// Change selection.
 		changeDomSelection();
@@ -314,15 +319,13 @@ describe( 'SelectionObserver', () => {
 		viewDocument.on( 'selectionChange', () => {
 			// Manually set selection because no handlers are set for selectionChange event in this test.
 			// Normally this is handled by view -> model -> view selection converters chain.
-			const viewSel = viewDocument.selection;
-
-			const viewAnchor = viewDocument.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset );
-			const viewFocus = viewDocument.domConverter.domPositionToView( sel.focusNode, sel.focusOffset );
+			const viewAnchor = view.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset );
+			const viewFocus = view.domConverter.domPositionToView( sel.focusNode, sel.focusOffset );
 
-			viewSel.setTo( viewAnchor );
-			viewSel.setFocus( viewFocus );
-
-			viewDocument.render();
+			view.change( writer => {
+				writer.setSelection( viewAnchor );
+				writer.setSelectionFocus( viewFocus );
+			} );
 		} );
 
 		viewDocument.once( 'selectionChange', () => {
@@ -330,7 +333,7 @@ describe( 'SelectionObserver', () => {
 
 			selectionObserver.listenTo( domDocument, 'selectionchange', () => {
 				// 4. Check if view was re-rendered.
-				expect( viewDocument.render.called ).to.be.true;
+				expect( view.render.called ).to.be.true;
 
 				done();
 			}, { priority: 'lowest' } );
@@ -339,7 +342,7 @@ describe( 'SelectionObserver', () => {
 			// Current and new selection position are similar in view (but not equal!).
 			// Also add a spy to `viewDocument#render` to see if view will be re-rendered.
 			sel.collapse( domUI, 0 );
-			sinon.spy( viewDocument, 'render' );
+			sinon.spy( view, 'render' );
 
 			// Some browsers like Safari won't allow to put selection inside empty ui element.
 			// In that situation selection should stay in correct place.

+ 63 - 49
packages/ckeditor5-engine/tests/view/placeholder.js

@@ -5,139 +5,134 @@
 
 import { attachPlaceholder, detachPlaceholder } from '../../src/view/placeholder';
 import createViewRoot from './_utils/createroot';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewDocument from '../../src/view/document';
+import View from '../../src/view/view';
 import ViewRange from '../../src/view/range';
 import { setData } from '../../src/dev-utils/view';
 
 describe( 'placeholder', () => {
-	let viewDocument, viewRoot;
+	let view, viewDocument, viewRoot;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		viewRoot = createViewRoot( viewDocument );
 		viewDocument.isFocused = true;
 	} );
 
 	describe( 'createPlaceholder', () => {
-		it( 'should throw if element is not inside document', () => {
-			const element = new ViewContainerElement( 'div' );
-
-			expect( () => {
-				attachPlaceholder( element, 'foo bar baz' );
-			} ).to.throw( 'view-placeholder-element-is-detached' );
-		} );
-
 		it( 'should attach proper CSS class and data attribute', () => {
-			setData( viewDocument, '<div></div><div>{another div}</div>' );
+			setData( view, '<div></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'foo bar baz' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
 		} );
 
 		it( 'if element has children set only data attribute', () => {
-			setData( viewDocument, '<div>first div</div><div>{another div}</div>' );
+			setData( view, '<div>first div</div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'foo bar baz' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
 		} );
 
 		it( 'if element has only ui elements, set CSS class and data attribute', () => {
-			setData( viewDocument, '<div><ui:span></ui:span><ui:span></ui:span></div><div>{another div}</div>' );
+			setData( view, '<div><ui:span></ui:span><ui:span></ui:span></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'foo bar baz' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
 		} );
 
 		it( 'if element has selection inside set only data attribute', () => {
-			setData( viewDocument, '<div>[]</div><div>another div</div>' );
+			setData( view, '<div>[]</div><div>another div</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'foo bar baz' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
 		} );
 
 		it( 'if element has selection inside but document is blurred should contain placeholder CSS class', () => {
-			setData( viewDocument, '<div>[]</div><div>another div</div>' );
+			setData( view, '<div>[]</div><div>another div</div>' );
 			const element = viewRoot.getChild( 0 );
 			viewDocument.isFocused = false;
 
-			attachPlaceholder( element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'foo bar baz' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
 		} );
 
 		it( 'use check function if one is provided', () => {
-			setData( viewDocument, '<div></div><div>{another div}</div>' );
+			setData( view, '<div></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 			const spy = sinon.spy( () => false );
 
-			attachPlaceholder( element, 'foo bar baz', spy );
+			attachPlaceholder( view, element, 'foo bar baz', spy );
 
-			sinon.assert.calledOnce( spy );
+			sinon.assert.called( spy );
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
 		} );
 
 		it( 'should remove CSS class if selection is moved inside', () => {
-			setData( viewDocument, '<div></div><div>{another div}</div>' );
+			setData( view, '<div></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'foo bar baz' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
 
-			viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] );
-			viewDocument.render();
+			view.change( writer => {
+				writer.setSelection( ViewRange.createIn( element ) );
+			} );
 
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
 		} );
 
 		it( 'should change placeholder settings when called twice', () => {
-			setData( viewDocument, '<div></div><div>{another div}</div>' );
+			setData( view, '<div></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
-			attachPlaceholder( element, 'new text' );
+			attachPlaceholder( view, element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'new text' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'new text' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
 		} );
 
 		it( 'should not throw when element is no longer in document', () => {
-			setData( viewDocument, '<div></div><div>{another div}</div>' );
+			setData( view, '<div></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
-			setData( viewDocument, '<p>paragraph</p>' );
+			attachPlaceholder( view, element, 'foo bar baz' );
+			setData( view, '<p>paragraph</p>' );
 
-			viewDocument.render();
+			view.render();
 		} );
 
 		it( 'should allow to add placeholder to elements from different documents', () => {
-			setData( viewDocument, '<div></div><div>{another div}</div>' );
+			setData( view, '<div></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
-			const secondDocument = new ViewDocument();
+
+			const secondView = new View();
+			const secondDocument = secondView.document;
 			secondDocument.isFocused = true;
 			const secondRoot = createViewRoot( secondDocument );
-			setData( secondDocument, '<div></div><div>{another div}</div>' );
+			setData( secondView, '<div></div><div>{another div}</div>' );
 			const secondElement = secondRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'first placeholder' );
-			attachPlaceholder( secondElement, 'second placeholder' );
+			attachPlaceholder( view, element, 'first placeholder' );
+			attachPlaceholder( secondView, secondElement, 'second placeholder' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
@@ -146,12 +141,13 @@ describe( 'placeholder', () => {
 			expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true;
 
 			// Move selection to the elements with placeholders.
-			viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] );
-			secondDocument.selection.setTo( [ ViewRange.createIn( secondElement ) ] );
+			view.change( writer => {
+				writer.setSelection( ViewRange.createIn( element ) );
+			} );
 
-			// Render changes.
-			viewDocument.render();
-			secondDocument.render();
+			secondView.change( writer => {
+				writer.setSelection( ViewRange.createIn( secondElement ) );
+			} );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
@@ -159,19 +155,37 @@ describe( 'placeholder', () => {
 			expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' );
 			expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.false;
 		} );
+
+		it( 'should update placeholder before rendering', () => {
+			setData( view, '<div></div><div>{another div}</div>' );
+			const element = viewRoot.getChild( 0 );
+
+			attachPlaceholder( view, element, 'foo bar baz' );
+
+			view.change( writer => {
+				writer.setSelection( ViewRange.createIn( element ) );
+
+				// Here we are before rendering - placeholder is visible in first element;
+				expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
+				expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
+			} );
+
+			// After rendering - placeholder should be invisible since selection is moved there.
+			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
+		} );
 	} );
 
 	describe( 'detachPlaceholder', () => {
 		it( 'should remove placeholder from element', () => {
-			setData( viewDocument, '<div></div><div>{another div}</div>' );
+			setData( view, '<div></div><div>{another div}</div>' );
 			const element = viewRoot.getChild( 0 );
 
-			attachPlaceholder( element, 'foo bar baz' );
+			attachPlaceholder( view, element, 'foo bar baz' );
 
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
 
-			detachPlaceholder( element );
+			detachPlaceholder( view, element );
 
 			expect( element.hasAttribute( 'data-placeholder' ) ).to.be.false;
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;

+ 1 - 3
packages/ckeditor5-engine/tests/view/position.js

@@ -511,12 +511,10 @@ describe( 'Position', () => {
 			const document = new Document();
 			const p = new Element( 'p' );
 			const editable = new EditableElement( 'div', null, p );
-			editable.document = document;
+			editable._document = document;
 			const position = new Position( p, 0 );
 
 			expect( position.editableElement ).to.equal( editable );
-
-			document.destroy();
 		} );
 	} );
 

+ 125 - 108
packages/ckeditor5-engine/tests/view/renderer.js

@@ -5,7 +5,7 @@
 
 /* globals document, window, NodeFilter */
 
-import ViewDocument from '../../src/view/document';
+import View from '../../src/view/view';
 import ViewElement from '../../src/view/element';
 import ViewContainerElement from '../../src/view/containerelement';
 import ViewAttributeElement from '../../src/view/attributeelement';
@@ -21,7 +21,6 @@ import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, BR_FILLER } from '.
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import createViewRoot from './_utils/createroot';
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-import { unwrap, insert, remove } from '../../src/view/writer';
 import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';
 
 testUtils.createSinonSandbox();
@@ -52,7 +51,7 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should mark attributes which need update', () => {
-			viewRoot.setAttribute( 'class', 'foo' );
+			viewRoot._setAttribute( 'class', 'foo' );
 
 			renderer.markToSync( 'attributes', viewRoot );
 
@@ -122,8 +121,8 @@ describe( 'Renderer', () => {
 			renderer.markedAttributes.clear();
 			renderer.markedChildren.clear();
 
-			selection.setTo( null );
-			selection.setFake( false );
+			selection._setTo( null );
+			selection._setFake( false );
 
 			selectionEditable = viewRoot;
 
@@ -142,7 +141,7 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should update attributes', () => {
-			viewRoot.setAttribute( 'class', 'foo' );
+			viewRoot._setAttribute( 'class', 'foo' );
 
 			renderer.markToSync( 'attributes', viewRoot );
 			renderer.render();
@@ -153,7 +152,7 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should remove attributes', () => {
-			viewRoot.setAttribute( 'class', 'foo' );
+			viewRoot._setAttribute( 'class', 'foo' );
 			domRoot.setAttribute( 'id', 'bar' );
 			domRoot.setAttribute( 'class', 'bar' );
 
@@ -405,7 +404,7 @@ describe( 'Renderer', () => {
 
 			const viewRoot = new ViewElement( 'p' );
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -419,7 +418,7 @@ describe( 'Renderer', () => {
 				'<container:p>foo<attribute:b contenteditable="false">[]</attribute:b>bar</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -441,7 +440,7 @@ describe( 'Renderer', () => {
 				'<container:p contenteditable="false">foo<attribute:b>[]</attribute:b>bar</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -466,7 +465,7 @@ describe( 'Renderer', () => {
 				'<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -495,7 +494,7 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p>foo{}<b></b></p>
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) );
 
 			renderer.render();
 
@@ -525,7 +524,7 @@ describe( 'Renderer', () => {
 				'<container:p>[]<attribute:b>foo</attribute:b></container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -548,7 +547,7 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p><b>{}foo</b></p>
-			selection.setTo( ViewRange.createFromParentsAndOffsets(
+			selection._setTo( ViewRange.createFromParentsAndOffsets(
 				viewP.getChild( 0 ).getChild( 0 ), 0, viewP.getChild( 0 ).getChild( 0 ), 0 ) );
 
 			renderer.render();
@@ -576,7 +575,7 @@ describe( 'Renderer', () => {
 				'<container:p><attribute:b>foo</attribute:b>[]</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -599,7 +598,7 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p><b>foo{}</b></p>
-			selection.setTo( ViewRange.createFromParentsAndOffsets(
+			selection._setTo( ViewRange.createFromParentsAndOffsets(
 				viewP.getChild( 0 ).getChild( 0 ), 3, viewP.getChild( 0 ).getChild( 0 ), 3 ) );
 
 			renderer.render();
@@ -626,7 +625,7 @@ describe( 'Renderer', () => {
 				'<container:p><attribute:b>foo</attribute:b>[]<attribute:b>bar</attribute:b></container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -654,7 +653,7 @@ describe( 'Renderer', () => {
 				'<container:p>foo<attribute:b>[]</attribute:b><attribute:i></attribute:i></container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 
@@ -672,7 +671,7 @@ describe( 'Renderer', () => {
 
 			// Step 2: <p>foo<b></b><i>"FILLER{}"</i></p>
 			const viewI = viewP.getChild( 2 );
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
 
 			renderer.render();
 
@@ -690,7 +689,7 @@ describe( 'Renderer', () => {
 			const { view: viewP, selection: newSelection } = parse( '<container:p>foo<attribute:b>[]</attribute:b></container:p>' );
 			const viewB = viewP.getChild( 1 );
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -705,13 +704,13 @@ describe( 'Renderer', () => {
 			// Step 2: Add text node.
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewB );
 			renderer.render();
 
 			// Step 3: Remove selection from the view.
-			selection.setTo( null );
+			selection._setTo( null );
 
 			renderer.render();
 
@@ -729,7 +728,7 @@ describe( 'Renderer', () => {
 			// Step 1: <p>bar<b>foo</b>"FILLER{}"</p>
 			const { view: viewP, selection: newSelection } = parse( '<container:p>bar<attribute:b>foo</attribute:b>[]</container:p>' );
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -741,7 +740,7 @@ describe( 'Renderer', () => {
 			// Step 2: Remove the <b> and update the selection (<p>bar[]</p>).
 			viewP.removeChildren( 1 );
 
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -758,7 +757,7 @@ describe( 'Renderer', () => {
 				'<container:p><attribute:b>foo</attribute:b>[]<attribute:b>bar</attribute:b></container:p><container:p></container:p>'
 			);
 			viewRoot.appendChildren( viewFragment );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -778,7 +777,7 @@ describe( 'Renderer', () => {
 
 			viewP2.appendChildren( removedChildren );
 
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.markToSync( 'children', viewP2 );
@@ -800,7 +799,7 @@ describe( 'Renderer', () => {
 			// Step 1: <p>bar<b>"FILLER{}"</b></p>
 			const { view: viewP, selection: newSelection } = parse( '<container:p>bar<attribute:b>[]</attribute:b></container:p>' );
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -815,7 +814,7 @@ describe( 'Renderer', () => {
 			const viewI = parse( '<attribute:i></attribute:i>' );
 			viewP.appendChildren( viewI );
 
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -832,7 +831,7 @@ describe( 'Renderer', () => {
 			// Step 1: <p>bar<b>abc</b>"FILLER"{}</p>
 			const { view: viewP, selection: newSelection } = parse( '<container:p>bar<attribute:b>abc</attribute:b>[]</container:p>' );
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -845,7 +844,7 @@ describe( 'Renderer', () => {
 			const viewAbc = parse( 'abc' );
 			viewP.appendChildren( viewAbc );
 
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -861,7 +860,7 @@ describe( 'Renderer', () => {
 			const { view: viewP, selection: newSelection } = parse( '<container:p>[]</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -888,7 +887,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewP.appendChildren( viewText );
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
@@ -900,7 +899,7 @@ describe( 'Renderer', () => {
 			const { view: viewP, selection: newSelection } = parse( '<container:p>[]</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -918,7 +917,7 @@ describe( 'Renderer', () => {
 			// Add text node only in View <p>x{}</p>
 			const viewText = new ViewText( 'x' );
 			viewP.appendChildren( viewText );
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -938,7 +937,7 @@ describe( 'Renderer', () => {
 			const { view: viewP, selection: newSelection } = parse( '<container:p>x{}</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -965,7 +964,7 @@ describe( 'Renderer', () => {
 
 			viewP.removeChildren( 0 );
 
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
@@ -980,7 +979,7 @@ describe( 'Renderer', () => {
 				'<container:p><attribute:b>[]</attribute:b>foo</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -1016,7 +1015,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
@@ -1031,7 +1030,7 @@ describe( 'Renderer', () => {
 				'<container:p><attribute:b>[]</attribute:b>foo</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -1059,7 +1058,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewB );
 			renderer.render();
@@ -1094,7 +1093,7 @@ describe( 'Renderer', () => {
 				'<container:p><attribute:b>[]</attribute:b>foo</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -1122,7 +1121,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'text', viewText );
 			renderer.render();
@@ -1145,7 +1144,7 @@ describe( 'Renderer', () => {
 				'<container:p>fo{o<attribute:b>b}ar</attribute:b></container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -1183,7 +1182,7 @@ describe( 'Renderer', () => {
 			const { view: viewP, selection: newSelection } = parse( '<container:p>fo{o}</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.render();
 
@@ -1225,7 +1224,7 @@ describe( 'Renderer', () => {
 			const { view: viewP, selection: newSelection } = parse( '<container:p>fo{o}</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.render();
 
@@ -1254,7 +1253,7 @@ describe( 'Renderer', () => {
 			const { view: viewP, selection: newSelection } = parse( '<container:p>foo[]</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -1269,7 +1268,7 @@ describe( 'Renderer', () => {
 				'<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
 
 			viewRoot.appendChildren( viewP );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -1282,7 +1281,7 @@ describe( 'Renderer', () => {
 			// Remove filler.
 			domB.childNodes[ 0 ].data = '';
 
-			selection.setTo( null );
+			selection._setTo( null );
 			renderer.markToSync( 'children', viewB );
 
 			expect( () => {
@@ -1302,7 +1301,7 @@ describe( 'Renderer', () => {
 			const { view: view, selection: newSelection } = parse( inputView );
 
 			viewRoot.appendChildren( view );
-			selection.setTo( newSelection );
+			selection._setTo( newSelection );
 
 			renderer.markToSync( 'children', viewRoot );
 			renderer.render();
@@ -1363,14 +1362,14 @@ describe( 'Renderer', () => {
 					'<container:p>[foo bar]</container:p>'
 				);
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
 			} );
 
 			it( 'should render fake selection', () => {
 				const label = 'fake selection label';
-				selection.setFake( true, { label } );
+				selection._setFake( true, { label } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -1387,7 +1386,7 @@ describe( 'Renderer', () => {
 			} );
 
 			it( 'should render &nbsp; if no selection label is provided', () => {
-				selection.setFake( true );
+				selection._setFake( true );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -1403,10 +1402,10 @@ describe( 'Renderer', () => {
 			} );
 
 			it( 'should remove fake selection container when selection is no longer fake', () => {
-				selection.setFake( true );
+				selection._setFake( true );
 				renderer.render();
 
-				selection.setFake( false );
+				selection._setFake( false );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 1 );
@@ -1422,14 +1421,14 @@ describe( 'Renderer', () => {
 			it( 'should reuse fake selection container #1', () => {
 				const label = 'fake selection label';
 
-				selection.setFake( true, { label } );
+				selection._setFake( true, { label } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
 
 				const container = domRoot.childNodes[ 1 ];
 
-				selection.setFake( true, { label } );
+				selection._setFake( true, { label } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -1443,19 +1442,19 @@ describe( 'Renderer', () => {
 			} );
 
 			it( 'should reuse fake selection container #2', () => {
-				selection.setFake( true, { label: 'label 1' } );
+				selection._setFake( true, { label: 'label 1' } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
 
 				const container = domRoot.childNodes[ 1 ];
 
-				selection.setFake( false );
+				selection._setFake( false );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 1 );
 
-				selection.setFake( true, { label: 'label 2' } );
+				selection._setFake( true, { label: 'label 2' } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -1469,14 +1468,14 @@ describe( 'Renderer', () => {
 			} );
 
 			it( 'should reuse fake selection container #3', () => {
-				selection.setFake( true, { label: 'label 1' } );
+				selection._setFake( true, { label: 'label 1' } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
 
 				const container = domRoot.childNodes[ 1 ];
 
-				selection.setFake( true, { label: 'label 2' } );
+				selection._setFake( true, { label: 'label 2' } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -1490,7 +1489,7 @@ describe( 'Renderer', () => {
 			} );
 
 			it( 'should style fake selection container properly', () => {
-				selection.setFake( true, { label: 'fake selection' } );
+				selection._setFake( true, { label: 'fake selection' } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -1503,7 +1502,7 @@ describe( 'Renderer', () => {
 			} );
 
 			it( 'should bind fake selection container to view selection', () => {
-				selection.setFake( true, { label: 'fake selection' } );
+				selection._setFake( true, { label: 'fake selection' } );
 				renderer.render();
 
 				expect( domRoot.childNodes.length ).to.equal( 2 );
@@ -1553,7 +1552,7 @@ describe( 'Renderer', () => {
 					'<container:p>foo{}<attribute:b>bar</attribute:b></container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1573,7 +1572,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo<attribute:b>{}bar</attribute:b></container:p>
-				selection.setTo( [
+				selection._setTo( [
 					new ViewRange( new ViewPosition( viewB.getChild( 0 ), 0 ), new ViewPosition( viewB.getChild( 0 ), 0 ) )
 				] );
 
@@ -1593,7 +1592,7 @@ describe( 'Renderer', () => {
 					'<container:p>foo<attribute:b>[]</attribute:b></container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1612,7 +1611,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo{}<attribute:b></attribute:b></container:p>
-				selection.setTo( [
+				selection._setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewP.getChild( 0 ), 3 ) )
 				] );
 
@@ -1632,7 +1631,7 @@ describe( 'Renderer', () => {
 					'<container:p>fo{o}<attribute:b>bar</attribute:b></container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1652,7 +1651,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>fo{o<attribute:b>b}ar</attribute:b></container:p>
-				selection.setTo( [
+				selection._setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 2 ), new ViewPosition( viewB.getChild( 0 ), 1 ) )
 				] );
 
@@ -1671,7 +1670,7 @@ describe( 'Renderer', () => {
 				const { view: viewP, selection: newSelection } = parse( '<container:p>foo[]<ui:span></ui:span></container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1713,7 +1712,7 @@ describe( 'Renderer', () => {
 					'<container:p>foo<attribute:b>{ba}r</attribute:b></container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1733,7 +1732,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo{<attribute:b>ba}r</attribute:b></container:p>
-				selection.setTo( [
+				selection._setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewB.getChild( 0 ), 2 ) )
 				] );
 
@@ -1751,7 +1750,7 @@ describe( 'Renderer', () => {
 					'<container:p>foo<attribute:b>b{ar}</attribute:b>baz</container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1771,7 +1770,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo<attribute:b>b{ar</attribute:b>}baz</container:p>
-				selection.setTo( [
+				selection._setTo( [
 					new ViewRange( new ViewPosition( viewB.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) )
 				] );
 
@@ -1789,7 +1788,7 @@ describe( 'Renderer', () => {
 					'<container:p>foo<attribute:b><attribute:i>{ba}r</attribute:i></attribute:b></container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1809,7 +1808,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo{<attribute:b><attribute:i>ba}r</attribute:i></attribute:b></container:p>
-				selection.setTo( [
+				selection._setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewI.getChild( 0 ), 2 ) )
 				] );
 
@@ -1827,7 +1826,7 @@ describe( 'Renderer', () => {
 					'<container:p>f{oo<attribute:b><attribute:i>bar}</attribute:i></attribute:b>baz</container:p>' );
 
 				viewRoot.appendChildren( viewP );
-				selection.setTo( newSelection );
+				selection._setTo( newSelection );
 
 				renderer.markToSync( 'children', viewRoot );
 				renderer.render();
@@ -1846,7 +1845,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>f{oo<attribute:b><attribute:i>bar</attribute:i></attribute:b>}baz</container:p>
-				selection.setTo( [
+				selection._setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) )
 				] );
 
@@ -1860,19 +1859,20 @@ describe( 'Renderer', () => {
 	} );
 
 	describe( '#922', () => {
-		let viewDoc, viewRoot, domRoot, converter;
+		let view, viewDoc, viewRoot, domRoot, converter;
 
 		beforeEach( () => {
-			viewDoc = new ViewDocument();
+			view = new View();
+			viewDoc = view.document;
 			domRoot = document.createElement( 'div' );
 			document.body.appendChild( domRoot );
 			viewRoot = createViewRoot( viewDoc );
-			viewDoc.attachDomRoot( domRoot );
-			converter = viewDoc.domConverter;
+			view.attachDomRoot( domRoot );
+			converter = view.domConverter;
 		} );
 
 		it( 'should properly render unwrapped attributes #1', () => {
-			setViewData( viewDoc,
+			setViewData( view,
 				'<container:p>' +
 					'[<attribute:italic>' +
 						'<attribute:strong>f</attribute:strong>' +
@@ -1882,14 +1882,17 @@ describe( 'Renderer', () => {
 			);
 
 			// Render it to DOM to create initial DOM <-> view mappings.
-			viewDoc.render();
+			view.render();
 
 			// Unwrap italic attribute element.
-			unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) );
-			expect( getViewData( viewDoc ) ).to.equal( '<p>[<strong>foo</strong>]</p>' );
+			view.change( writer => {
+				writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) );
+			} );
+
+			expect( getViewData( view ) ).to.equal( '<p>[<strong>foo</strong>]</p>' );
 
 			// Re-render changes in view to DOM.
-			viewDoc.render();
+			view.render();
 
 			// Check if DOM is rendered correctly.
 			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><strong>foo</strong></p>' );
@@ -1897,7 +1900,7 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should properly render unwrapped attributes #2', () => {
-			setViewData( viewDoc,
+			setViewData( view,
 				'<container:p>' +
 					'[<attribute:italic>' +
 						'<attribute:strong>foo</attribute:strong>' +
@@ -1905,15 +1908,18 @@ describe( 'Renderer', () => {
 				'</container:p>' );
 
 			// Render it to DOM to create initial DOM <-> view mappings.
-			viewDoc.render();
+			view.render();
 
 			// Unwrap italic attribute element and change text inside.
-			unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) );
+			view.change( writer => {
+				writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) );
+			} );
+
 			viewRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ).data = 'bar';
-			expect( getViewData( viewDoc ) ).to.equal( '<p>[<strong>bar</strong>]</p>' );
+			expect( getViewData( view ) ).to.equal( '<p>[<strong>bar</strong>]</p>' );
 
 			// Re-render changes in view to DOM.
-			viewDoc.render();
+			view.render();
 
 			// Check if DOM is rendered correctly.
 			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><strong>bar</strong></p>' );
@@ -1921,21 +1927,25 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should properly render if text is changed and element is inserted into same node #1', () => {
-			setViewData( viewDoc,
+			setViewData( view,
 				'<container:p>foo</container:p>'
 			);
 
 			// Render it to DOM to create initial DOM <-> view mappings.
-			viewDoc.render();
+			view.render();
 
 			// Change text and insert new element into paragraph.
 			const textNode = viewRoot.getChild( 0 ).getChild( 0 );
 			textNode.data = 'foobar';
-			insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) );
-			expect( getViewData( viewDoc ) ).to.equal( '<p>foobar<img></img></p>' );
+
+			view.change( writer => {
+				writer.insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) );
+			} );
+
+			expect( getViewData( view ) ).to.equal( '<p>foobar<img></img></p>' );
 
 			// Re-render changes in view to DOM.
-			viewDoc.render();
+			view.render();
 
 			// Check if DOM is rendered correctly.
 			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p>foobar<img></img></p>' );
@@ -1943,21 +1953,25 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should properly render if text is changed and element is inserted into same node #2', () => {
-			setViewData( viewDoc,
+			setViewData( view,
 				'<container:p>foo</container:p>'
 			);
 
 			// Render it to DOM to create initial DOM <-> view mappings.
-			viewDoc.render();
+			view.render();
 
 			// Change text and insert new element into paragraph.
 			const textNode = viewRoot.getChild( 0 ).getChild( 0 );
 			textNode.data = 'foobar';
-			insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) );
-			expect( getViewData( viewDoc ) ).to.equal( '<p><img></img>foobar</p>' );
+
+			view.change( writer => {
+				writer.insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) );
+			} );
+
+			expect( getViewData( view ) ).to.equal( '<p><img></img>foobar</p>' );
 
 			// Re-render changes in view to DOM.
-			viewDoc.render();
+			view.render();
 
 			// Check if DOM is rendered correctly.
 			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><img></img>foobar</p>' );
@@ -1965,7 +1979,7 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should not unbind elements that are removed and reinserted to DOM', () => {
-			setViewData( viewDoc,
+			setViewData( view,
 				'<container:p>' +
 					'<attribute:b></attribute:b>' +
 					'<attribute:i></attribute:i>' +
@@ -1974,18 +1988,21 @@ describe( 'Renderer', () => {
 			);
 
 			// Render it to DOM to create initial DOM <-> view mappings.
-			viewDoc.render();
+			view.render();
 
 			// Remove first element and reinsert it at the end.
 			const container = viewRoot.getChild( 0 );
 			const firstElement = container.getChild( 0 );
 
-			remove( ViewRange.createOn( firstElement ) );
-			insert( new ViewPosition( container, 2 ), firstElement );
-			expect( getViewData( viewDoc ) ).to.equal( '<p><i></i><span></span><b></b></p>' );
+			view.change( writer => {
+				writer.remove( ViewRange.createOn( firstElement ) );
+				writer.insert( new ViewPosition( container, 2 ), firstElement );
+			} );
+
+			expect( getViewData( view ) ).to.equal( '<p><i></i><span></span><b></b></p>' );
 
 			// Re-render changes in view to DOM.
-			viewDoc.render();
+			view.render();
 
 			// Check if DOM is rendered correctly.
 			expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '<p><i></i><span></span><b></b></p>' );

+ 4 - 4
packages/ckeditor5-engine/tests/view/rooteditableelement.js

@@ -13,7 +13,7 @@ describe( 'RootEditableElement', () => {
 	describe( 'constructor()', () => {
 		it( 'should create an element with default root name', () => {
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 			expect( root ).to.be.instanceof( EditableElement );
 			expect( root ).to.be.instanceof( ContainerElement );
@@ -27,7 +27,7 @@ describe( 'RootEditableElement', () => {
 
 		it( 'should create an element with custom root name', () => {
 			const root = new RootEditableElement( 'h1' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 			root.rootName = 'header';
 
 			expect( root.rootName ).to.equal( 'header' );
@@ -83,12 +83,12 @@ describe( 'RootEditableElement', () => {
 
 	it( 'should be cloned properly', () => {
 		const root = new RootEditableElement( 'h1' );
-		root.document = createDocumentMock();
+		root._document = createDocumentMock();
 		root.rootName = 'header';
 
 		const newRoot = root.clone();
 
-		expect( newRoot.document ).to.equal( root.document );
+		expect( newRoot._document ).to.equal( root._document );
 		expect( newRoot.rootName ).to.equal( root.rootName );
 	} );
 } );

+ 99 - 101
packages/ckeditor5-engine/tests/view/selection.js

@@ -94,7 +94,7 @@ describe( 'Selection', () => {
 
 		it( 'should be able to create a fake selection from the other fake selection', () => {
 			const otherSelection = new Selection( [ range2, range3 ], true );
-			otherSelection.setFake( true, { label: 'foo bar baz' } );
+			otherSelection._setFake( true, { label: 'foo bar baz' } );
 			const selection = new Selection( otherSelection );
 
 			expect( selection.isFake ).to.be.true;
@@ -132,7 +132,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return start of single range in selection', () => {
-			selection.setTo( range1 );
+			selection._setTo( range1 );
 			const anchor = selection.anchor;
 
 			expect( anchor.isEqual( range1.start ) ).to.be.true;
@@ -140,7 +140,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return end of single range in selection when added as backward', () => {
-			selection.setTo( range1, true );
+			selection._setTo( range1, true );
 			const anchor = selection.anchor;
 
 			expect( anchor.isEqual( range1.end ) ).to.be.true;
@@ -148,7 +148,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should get anchor from last inserted range', () => {
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			expect( selection.anchor.isEqual( range2.start ) ).to.be.true;
 		} );
@@ -160,14 +160,14 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return end of single range in selection', () => {
-			selection.setTo( range1 );
+			selection._setTo( range1 );
 			const focus = selection.focus;
 
 			expect( focus.isEqual( range1.end ) ).to.be.true;
 		} );
 
 		it( 'should return start of single range in selection when added as backward', () => {
-			selection.setTo( range1, true );
+			selection._setTo( range1, true );
 			const focus = selection.focus;
 
 			expect( focus.isEqual( range1.start ) ).to.be.true;
@@ -175,16 +175,16 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should get focus from last inserted range', () => {
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			expect( selection.focus.isEqual( range2.end ) ).to.be.true;
 		} );
 	} );
 
-	describe( 'setFocus', () => {
+	describe( '_setFocus()', () => {
 		it( 'keeps all existing ranges when no modifications needed', () => {
-			selection.setTo( range1 );
-			selection.setFocus( selection.focus );
+			selection._setTo( range1 );
+			selection._setFocus( selection.focus );
 
 			expect( count( selection.getRanges() ) ).to.equal( 1 );
 		} );
@@ -193,7 +193,7 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 'end' );
 
 			expect( () => {
-				selection.setFocus( endPos );
+				selection._setFocus( endPos );
 			} ).to.throw( CKEditorError, /view-selection-setFocus-no-ranges/ );
 		} );
 
@@ -201,9 +201,9 @@ describe( 'Selection', () => {
 			const startPos = Position.createAt( el, 1 );
 			const endPos = Position.createAt( el, 2 );
 
-			selection.setTo( startPos );
+			selection._setTo( startPos );
 
-			selection.setFocus( endPos );
+			selection._setFocus( endPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -213,9 +213,9 @@ describe( 'Selection', () => {
 			const startPos = Position.createAt( el, 1 );
 			const endPos = Position.createAt( el, 0 );
 
-			selection.setTo( startPos );
+			selection._setTo( startPos );
 
-			selection.setFocus( endPos );
+			selection._setFocus( endPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -227,9 +227,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 3 );
 
-			selection.setTo( new Range( startPos, endPos ) );
+			selection._setTo( new Range( startPos, endPos ) );
 
-			selection.setFocus( newEndPos );
+			selection._setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -240,9 +240,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 0 );
 
-			selection.setTo( new Range( startPos, endPos ) );
+			selection._setTo( new Range( startPos, endPos ) );
 
-			selection.setFocus( newEndPos );
+			selection._setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -254,9 +254,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 3 );
 
-			selection.setTo( new Range( startPos, endPos ), true );
+			selection._setTo( new Range( startPos, endPos ), true );
 
-			selection.setFocus( newEndPos );
+			selection._setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -268,9 +268,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 0 );
 
-			selection.setTo( new Range( startPos, endPos ), true );
+			selection._setTo( new Range( startPos, endPos ), true );
 
-			selection.setFocus( newEndPos );
+			selection._setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -286,12 +286,12 @@ describe( 'Selection', () => {
 
 			const newEndPos = Position.createAt( el, 0 );
 
-			selection.setTo( [
+			selection._setTo( [
 				new Range( startPos1, endPos1 ),
 				new Range( startPos2, endPos2 )
 			] );
 
-			selection.setFocus( newEndPos );
+			selection._setFocus( newEndPos );
 
 			const ranges = Array.from( selection.getRanges() );
 
@@ -308,9 +308,9 @@ describe( 'Selection', () => {
 			const startPos = Position.createAt( el, 1 );
 			const endPos = Position.createAt( el, 2 );
 
-			selection.setTo( new Range( startPos, endPos ) );
+			selection._setTo( new Range( startPos, endPos ) );
 
-			selection.setFocus( startPos );
+			selection._setFocus( startPos );
 
 			expect( selection.focus.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.isCollapsed ).to.be.true;
@@ -323,8 +323,8 @@ describe( 'Selection', () => {
 
 			const spy = sinon.stub( Position, 'createAt' ).returns( newEndPos );
 
-			selection.setTo( new Range( startPos, endPos ) );
-			selection.setFocus( el, 'end' );
+			selection._setTo( new Range( startPos, endPos ) );
+			selection._setFocus( el, 'end' );
 
 			expect( spy.calledOnce ).to.be.true;
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -336,7 +336,7 @@ describe( 'Selection', () => {
 	describe( 'isCollapsed', () => {
 		it( 'should return true when there is single collapsed range', () => {
 			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
-			selection.setTo( range );
+			selection._setTo( range );
 
 			expect( selection.isCollapsed ).to.be.true;
 		} );
@@ -344,14 +344,14 @@ describe( 'Selection', () => {
 		it( 'should return false when there are multiple ranges', () => {
 			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
 			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 15 );
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			expect( selection.isCollapsed ).to.be.false;
 		} );
 
 		it( 'should return false when there is not collapsed range', () => {
 			const range = Range.createFromParentsAndOffsets( el, 15, el, 16 );
-			selection.setTo( range );
+			selection._setTo( range );
 
 			expect( selection.isCollapsed ).to.be.false;
 		} );
@@ -361,11 +361,11 @@ describe( 'Selection', () => {
 		it( 'should return proper range count', () => {
 			expect( selection.rangeCount ).to.equal( 0 );
 
-			selection.setTo( range1 );
+			selection._setTo( range1 );
 
 			expect( selection.rangeCount ).to.equal( 1 );
 
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			expect( selection.rangeCount ).to.equal( 2 );
 		} );
@@ -376,17 +376,17 @@ describe( 'Selection', () => {
 			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );
 			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
 
-			selection.setTo( range1, true );
+			selection._setTo( range1, true );
 			expect( selection ).to.have.property( 'isBackward', true );
 
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 			expect( selection ).to.have.property( 'isBackward', false );
 		} );
 
 		it( 'is false when last range is collapsed', () => {
 			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
 
-			selection.setTo( range, true );
+			selection._setTo( range, true );
 
 			expect( selection.isBackward ).to.be.false;
 		} );
@@ -394,7 +394,7 @@ describe( 'Selection', () => {
 
 	describe( 'getRanges', () => {
 		it( 'should return iterator with copies of all ranges', () => {
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			const iterable = selection.getRanges();
 			const ranges = Array.from( iterable );
@@ -409,7 +409,7 @@ describe( 'Selection', () => {
 
 	describe( 'getFirstRange', () => {
 		it( 'should return copy of range with first position', () => {
-			selection.setTo( [ range1, range2, range3 ] );
+			selection._setTo( [ range1, range2, range3 ] );
 
 			const range = selection.getFirstRange();
 
@@ -424,7 +424,7 @@ describe( 'Selection', () => {
 
 	describe( 'getLastRange', () => {
 		it( 'should return copy of range with last position', () => {
-			selection.setTo( [ range1, range2, range3 ] );
+			selection._setTo( [ range1, range2, range3 ] );
 
 			const range = selection.getLastRange();
 
@@ -439,7 +439,7 @@ describe( 'Selection', () => {
 
 	describe( 'getFirstPosition', () => {
 		it( 'should return copy of first position', () => {
-			selection.setTo( [ range1, range2, range3 ] );
+			selection._setTo( [ range1, range2, range3 ] );
 
 			const position = selection.getFirstPosition();
 
@@ -454,7 +454,7 @@ describe( 'Selection', () => {
 
 	describe( 'getLastPosition', () => {
 		it( 'should return copy of range with last position', () => {
-			selection.setTo( [ range1, range2, range3 ] );
+			selection._setTo( [ range1, range2, range3 ] );
 
 			const position = selection.getLastPosition();
 
@@ -469,16 +469,16 @@ describe( 'Selection', () => {
 
 	describe( 'isEqual', () => {
 		it( 'should return true if selections equal', () => {
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection();
-			otherSelection.setTo( [ range1, range2 ] );
+			otherSelection._setTo( [ range1, range2 ] );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.true;
 		} );
 
 		it( 'should return true if backward selections equal', () => {
-			selection.setTo( range1, true );
+			selection._setTo( range1, true );
 
 			const otherSelection = new Selection( [ range1 ], true );
 
@@ -486,7 +486,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if ranges count does not equal', () => {
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection( [ range1 ] );
 
@@ -494,7 +494,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if ranges (other than the last added one) do not equal', () => {
-			selection.setTo( [ range1, range3 ] );
+			selection._setTo( [ range1, range3 ] );
 
 			const otherSelection = new Selection( [ range2, range3 ] );
 
@@ -502,7 +502,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if directions do not equal', () => {
-			selection.setTo( range1 );
+			selection._setTo( range1 );
 
 			const otherSelection = new Selection( [ range1 ], true );
 
@@ -511,25 +511,25 @@ describe( 'Selection', () => {
 
 		it( 'should return false if one selection is fake', () => {
 			const otherSelection = new Selection();
-			otherSelection.setFake( true );
+			otherSelection._setFake( true );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
 
 		it( 'should return true if both selection are fake', () => {
 			const otherSelection = new Selection( [ range1 ] );
-			otherSelection.setFake( true );
-			selection.setFake( true );
-			selection.setTo( range1 );
+			otherSelection._setFake( true );
+			selection._setFake( true );
+			selection._setTo( range1 );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.true;
 		} );
 
 		it( 'should return false if both selection are fake but have different label', () => {
 			const otherSelection = new Selection( [ range1 ] );
-			otherSelection.setFake( true, { label: 'foo bar baz' } );
-			selection.setFake( true );
-			selection.setTo( range1 );
+			otherSelection._setFake( true, { label: 'foo bar baz' } );
+			selection._setFake( true );
+			selection._setTo( range1 );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
@@ -543,7 +543,7 @@ describe( 'Selection', () => {
 
 	describe( 'isSimilar', () => {
 		it( 'should return true if selections equal', () => {
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection( [ range1, range2 ] );
 
@@ -551,7 +551,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if ranges count does not equal', () => {
-			selection.setTo( [ range1, range2 ] );
+			selection._setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection( [ range1 ] );
 
@@ -559,7 +559,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => {
-			selection.setTo( [ range1, range3 ] );
+			selection._setTo( [ range1, range3 ] );
 
 			const otherSelection = new Selection( [ range2, range3 ] );
 
@@ -567,7 +567,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if directions are not equal', () => {
-			selection.setTo( range1 );
+			selection._setTo( range1 );
 
 			const otherSelection = new Selection( [ range1 ], true );
 
@@ -597,7 +597,7 @@ describe( 'Selection', () => {
 			const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 );
 			const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 );
 
-			selection.setTo( [ rangeA1, rangeA2 ] );
+			selection._setTo( [ rangeA1, rangeA2 ] );
 
 			const otherSelection = new Selection( [ rangeB2, rangeB1 ] );
 
@@ -617,7 +617,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should add ranges and fire change event', done => {
-			selection.setTo( range1 );
+			selection._setTo( range1 );
 
 			selection.once( 'change', () => {
 				expect( selection.rangeCount ).to.equal( 2 );
@@ -641,14 +641,14 @@ describe( 'Selection', () => {
 		} );
 	} );
 
-	describe( 'setTo()', () => {
+	describe( '_setTo()', () => {
 		describe( 'simple scenarios', () => {
 			it( 'should set selection ranges from the given selection', () => {
-				selection.setTo( range1 );
+				selection._setTo( range1 );
 
 				const otherSelection = new Selection( [ range2, range3 ], true );
 
-				selection.setTo( otherSelection );
+				selection._setTo( otherSelection );
 
 				expect( selection.rangeCount ).to.equal( 2 );
 				expect( selection._ranges[ 0 ].isEqual( range2 ) ).to.be.true;
@@ -662,7 +662,7 @@ describe( 'Selection', () => {
 			it( 'should set selection on the given Range using _setRanges method', () => {
 				const spy = sinon.spy( selection, '_setRanges' );
 
-				selection.setTo( range1 );
+				selection._setTo( range1 );
 
 				expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] );
 				expect( selection.isBackward ).to.be.false;
@@ -673,7 +673,7 @@ describe( 'Selection', () => {
 			it( 'should set selection on the given iterable of Ranges using _setRanges method', () => {
 				const spy = sinon.spy( selection, '_setRanges' );
 
-				selection.setTo( new Set( [ range1, range2 ] ) );
+				selection._setTo( new Set( [ range1, range2 ] ) );
 
 				expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] );
 				expect( selection.isBackward ).to.be.false;
@@ -684,7 +684,7 @@ describe( 'Selection', () => {
 			it( 'should set collapsed selection on the given Position using _setRanges method', () => {
 				const spy = sinon.spy( selection, '_setRanges' );
 
-				selection.setTo( range1.start );
+				selection._setTo( range1.start );
 
 				expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
 				expect( Array.from( selection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start );
@@ -703,14 +703,14 @@ describe( 'Selection', () => {
 
 				const otherSelection = new Selection( [ range1 ] );
 
-				selection.setTo( otherSelection );
+				selection._setTo( otherSelection );
 			} );
 
 			it( 'should set fake state and label', () => {
 				const otherSelection = new Selection();
 				const label = 'foo bar baz';
-				otherSelection.setFake( true, { label } );
-				selection.setTo( otherSelection );
+				otherSelection._setFake( true, { label } );
+				selection._setTo( otherSelection );
 
 				expect( selection.isFake ).to.be.true;
 				expect( selection.fakeSelectionLabel ).to.equal( label );
@@ -720,7 +720,7 @@ describe( 'Selection', () => {
 				const otherSelection = new Selection();
 
 				expect( () => {
-					otherSelection.setTo( {} );
+					otherSelection._setTo( {} );
 				} ).to.throw( /view-selection-setTo-not-selectable/ );
 			} );
 
@@ -728,20 +728,20 @@ describe( 'Selection', () => {
 				const otherSelection = new Selection();
 
 				expect( () => {
-					otherSelection.setTo();
+					otherSelection._setTo();
 				} ).to.throw( /view-selection-setTo-not-selectable/ );
 			} );
 		} );
 
 		describe( 'setting collapsed selection', () => {
 			beforeEach( () => {
-				selection.setTo( [ range1, range2 ] );
+				selection._setTo( [ range1, range2 ] );
 			} );
 
 			it( 'should collapse selection at position', () => {
 				const position = new Position( el, 4 );
 
-				selection.setTo( position );
+				selection._setTo( position );
 				const range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( el );
@@ -753,14 +753,14 @@ describe( 'Selection', () => {
 				const foo = new Text( 'foo' );
 				const p = new Element( 'p', null, foo );
 
-				selection.setTo( foo );
+				selection._setTo( foo );
 				let range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( foo );
 				expect( range.start.offset ).to.equal( 0 );
 				expect( range.start.isEqual( range.end ) ).to.be.true;
 
-				selection.setTo( p, 1 );
+				selection._setTo( p, 1 );
 				range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( p );
@@ -772,21 +772,21 @@ describe( 'Selection', () => {
 				const foo = new Text( 'foo' );
 				const p = new Element( 'p', null, foo );
 
-				selection.setTo( foo, 'end' );
+				selection._setTo( foo, 'end' );
 				let range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( foo );
 				expect( range.start.offset ).to.equal( 3 );
 				expect( range.start.isEqual( range.end ) ).to.be.true;
 
-				selection.setTo( foo, 'before' );
+				selection._setTo( foo, 'before' );
 				range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( p );
 				expect( range.start.offset ).to.equal( 0 );
 				expect( range.start.isEqual( range.end ) ).to.be.true;
 
-				selection.setTo( foo, 'after' );
+				selection._setTo( foo, 'after' );
 				range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( p );
@@ -797,7 +797,7 @@ describe( 'Selection', () => {
 
 		describe( 'setting collapsed selection at start', () => {
 			it( 'should collapse to start position and fire change event', done => {
-				selection.setTo( [ range1, range2, range3 ] );
+				selection._setTo( [ range1, range2, range3 ] );
 				selection.once( 'change', () => {
 					expect( selection.rangeCount ).to.equal( 1 );
 					expect( selection.isCollapsed ).to.be.true;
@@ -805,13 +805,13 @@ describe( 'Selection', () => {
 					done();
 				} );
 
-				selection.setTo( selection.getFirstPosition() );
+				selection._setTo( selection.getFirstPosition() );
 			} );
 
 			it( 'should do nothing if no ranges present', () => {
 				const fireSpy = sinon.spy( selection, 'fire' );
 
-				selection.setTo( selection.getFirstPosition() );
+				selection._setTo( selection.getFirstPosition() );
 
 				fireSpy.restore();
 				expect( fireSpy.notCalled ).to.be.true;
@@ -820,7 +820,7 @@ describe( 'Selection', () => {
 
 		describe( 'setting collapsed selection to end', () => {
 			it( 'should collapse to end position and fire change event', done => {
-				selection.setTo( [ range1, range2, range3 ] );
+				selection._setTo( [ range1, range2, range3 ] );
 				selection.once( 'change', () => {
 					expect( selection.rangeCount ).to.equal( 1 );
 					expect( selection.isCollapsed ).to.be.true;
@@ -828,13 +828,13 @@ describe( 'Selection', () => {
 					done();
 				} );
 
-				selection.setTo( selection.getLastPosition() );
+				selection._setTo( selection.getLastPosition() );
 			} );
 
 			it( 'should do nothing if no ranges present', () => {
 				const fireSpy = sinon.spy( selection, 'fire' );
 
-				selection.setTo( selection.getLastPosition() );
+				selection._setTo( selection.getLastPosition() );
 
 				fireSpy.restore();
 				expect( fireSpy.notCalled ).to.be.true;
@@ -843,19 +843,19 @@ describe( 'Selection', () => {
 
 		describe( 'removing all ranges', () => {
 			it( 'should remove all ranges and fire change event', done => {
-				selection.setTo( [ range1, range2 ] );
+				selection._setTo( [ range1, range2 ] );
 
 				selection.once( 'change', () => {
 					expect( selection.rangeCount ).to.equal( 0 );
 					done();
 				} );
 
-				selection.setTo( null );
+				selection._setTo( null );
 			} );
 
 			it( 'should do nothing when no ranges are present', () => {
 				const fireSpy = sinon.spy( selection, 'fire' );
-				selection.setTo( null );
+				selection._setTo( null );
 
 				fireSpy.restore();
 				expect( fireSpy.notCalled ).to.be.true;
@@ -863,13 +863,13 @@ describe( 'Selection', () => {
 		} );
 	} );
 
-	describe( 'getEditableElement', () => {
+	describe( 'getEditableElement()', () => {
 		it( 'should return null if no ranges in selection', () => {
 			expect( selection.editableElement ).to.be.null;
 		} );
 
 		it( 'should return null if selection is placed in container that is not EditableElement', () => {
-			selection.setTo( range1 );
+			selection._setTo( range1 );
 
 			expect( selection.editableElement ).to.be.null;
 		} );
@@ -881,11 +881,9 @@ describe( 'Selection', () => {
 			const element = new Element( 'p' );
 			root.appendChildren( element );
 
-			selection.setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
+			selection._setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
 
 			expect( selection.editableElement ).to.equal( root );
-
-			viewDocument.destroy();
 		} );
 	} );
 
@@ -895,31 +893,31 @@ describe( 'Selection', () => {
 		} );
 	} );
 
-	describe( 'setFake', () => {
+	describe( '_setFake()', () => {
 		it( 'should allow to set selection to fake', () => {
-			selection.setFake( true );
+			selection._setFake( true );
 
 			expect( selection.isFake ).to.be.true;
 		} );
 
 		it( 'should allow to set fake selection label', () => {
 			const label = 'foo bar baz';
-			selection.setFake( true, { label } );
+			selection._setFake( true, { label } );
 
 			expect( selection.fakeSelectionLabel ).to.equal( label );
 		} );
 
 		it( 'should not set label when set to false', () => {
 			const label = 'foo bar baz';
-			selection.setFake( false, { label } );
+			selection._setFake( false, { label } );
 
 			expect( selection.fakeSelectionLabel ).to.equal( '' );
 		} );
 
 		it( 'should reset label when set to false', () => {
 			const label = 'foo bar baz';
-			selection.setFake( true, { label } );
-			selection.setFake( false );
+			selection._setFake( true, { label } );
+			selection._setFake( false );
 
 			expect( selection.fakeSelectionLabel ).to.equal( '' );
 		} );
@@ -932,11 +930,11 @@ describe( 'Selection', () => {
 				done();
 			} );
 
-			selection.setFake( true, { label: 'foo bar baz' } );
+			selection._setFake( true, { label: 'foo bar baz' } );
 		} );
 	} );
 
-	describe( 'getSelectedElement', () => {
+	describe( 'getSelectedElement()', () => {
 		it( 'should return selected element', () => {
 			const { selection, view } = parse( 'foo [<b>bar</b>] baz' );
 			const b = view.getChild( 1 );

+ 2 - 2
packages/ckeditor5-engine/tests/view/textproxy.js

@@ -92,7 +92,7 @@ describe( 'TextProxy', () => {
 		it( 'should return Document attached to the parent element', () => {
 			const docMock = createDocumentMock();
 			const root = new RootEditableElement( 'div' );
-			root.document = docMock;
+			root._document = docMock;
 
 			wrapper.parent = root;
 
@@ -109,7 +109,7 @@ describe( 'TextProxy', () => {
 	describe( 'getRoot', () => {
 		it( 'should return root element', () => {
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 			wrapper.parent = root;
 

+ 0 - 4
packages/ckeditor5-engine/tests/view/treewalker.js

@@ -48,10 +48,6 @@ describe( 'TreeWalker', () => {
 		rootEnding = new Position( root, 2 );
 	} );
 
-	afterEach( () => {
-		doc.destroy();
-	} );
-
 	describe( 'constructor()', () => {
 		it( 'should throw if neither boundaries nor starting position is set', () => {
 			expect( () => {

+ 39 - 38
packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js → packages/ckeditor5-engine/tests/view/view/jumpoverinlinefiller.js

@@ -5,8 +5,7 @@
 
 /* globals document */
 
-import ViewRange from '../../../src/view/range';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { INLINE_FILLER_LENGTH, isInlineFiller, startsWithFiller } from '../../../src/view/filler';
 
 import createViewRoot from '../_utils/createroot';
@@ -15,8 +14,8 @@ import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 
 import { parse, setData } from '../../../src/dev-utils/view';
 
-describe( 'Document', () => {
-	let viewDocument, domRoot;
+describe( 'View', () => {
+	let view, viewDocument, domRoot;
 
 	beforeEach( () => {
 		domRoot = createElement( document, 'div', {
@@ -24,9 +23,10 @@ describe( 'Document', () => {
 		} );
 		document.body.appendChild( domRoot );
 
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domRoot );
+		view.attachDomRoot( domRoot );
 
 		document.getSelection().removeAllRanges();
 
@@ -34,17 +34,17 @@ describe( 'Document', () => {
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 
 		domRoot.parentElement.removeChild( domRoot );
 	} );
 
 	describe( 'jump over inline filler hack', () => {
 		it( 'should jump over inline filler when left arrow is pressed after inline filler', () => {
-			setData( viewDocument, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
-			viewDocument.render();
+			setData( view, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			view.render();
 
-			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } );
 
 			const domSelection = document.getSelection();
 
@@ -63,10 +63,10 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should do nothing when another key is pressed', () => {
-			setData( viewDocument, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
-			viewDocument.render();
+			setData( view, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			view.render();
 
-			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) } );
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: view.domRoots.get( 'main' ) } );
 
 			const domSelection = document.getSelection();
 
@@ -76,10 +76,10 @@ describe( 'Document', () => {
 		} );
 
 		it( 'should do nothing if range is not collapsed', () => {
-			setData( viewDocument, '<container:p>foo<attribute:b>{x}</attribute:b>bar</container:p>' );
-			viewDocument.render();
+			setData( view, '<container:p>foo<attribute:b>{x}</attribute:b>bar</container:p>' );
+			view.render();
 
-			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } );
 
 			const domSelection = document.getSelection();
 
@@ -91,7 +91,7 @@ describe( 'Document', () => {
 
 		// See #664
 		// it( 'should do nothing if node does not start with the filler', () => {
-		// 	setData( viewDocument, '<container:p>foo<attribute:b>{}x</attribute:b>bar</container:p>' );
+		// 	setData( view, '<container:p>foo<attribute:b>{}x</attribute:b>bar</container:p>' );
 		// 	viewDocument.render();
 
 		// 	viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
@@ -104,29 +104,30 @@ describe( 'Document', () => {
 		// } );
 
 		it( 'should do nothing if caret is not directly before the filler', () => {
-			setData( viewDocument, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
-			viewDocument.render();
+			view.change( () => {
+				setData( view, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			} );
 
-			// Insert a letter to the <b>: '<container:p>foo<attribute:b>x{}</attribute:b>bar</container:p>'
-			// Do this both in the view and in the DOM to simulate typing and to avoid rendering (which would remove the filler).
-			const viewB = viewDocument.selection.getFirstPosition().parent;
-			const viewTextX = parse( 'x' );
-			viewB.appendChildren( viewTextX );
-			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) );
-
-			const domB = viewDocument.getDomRoot( 'main' ).querySelector( 'b' );
 			const domSelection = document.getSelection();
-			domB.childNodes[ 0 ].data += 'x';
-
-			const domRange = document.createRange();
-			domSelection.removeAllRanges();
-			domRange.setStart( domB.childNodes[ 0 ], INLINE_FILLER_LENGTH + 1 );
-			domRange.collapse( true );
-			domSelection.addRange( domRange );
-
-			viewDocument.render();
-
-			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
+			view.change( writer => {
+				// Insert a letter to the <b>: '<container:p>foo<attribute:b>x{}</attribute:b>bar</container:p>'
+				// Do this both in the view and in the DOM to simulate typing and to avoid rendering (which would remove the filler).
+				const viewB = writer.document.selection.getFirstPosition().parent;
+				const viewTextX = parse( 'x' );
+				viewB.appendChildren( viewTextX );
+				writer.setSelection( viewTextX, 1 );
+
+				const domB = view.getDomRoot( 'main' ).querySelector( 'b' );
+				domB.childNodes[ 0 ].data += 'x';
+
+				const domRange = document.createRange();
+				domSelection.removeAllRanges();
+				domRange.setStart( domB.childNodes[ 0 ], INLINE_FILLER_LENGTH + 1 );
+				domRange.collapse( true );
+				domSelection.addRange( domRange );
+			} );
+
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } );
 
 			expect( startsWithFiller( domSelection.anchorNode ) ).to.be.true;
 			expect( domSelection.anchorOffset ).to.equal( INLINE_FILLER_LENGTH + 1 );

+ 73 - 33
packages/ckeditor5-engine/tests/view/document/jumpoveruielement.js → packages/ckeditor5-engine/tests/view/view/jumpoveruielement.js

@@ -5,7 +5,7 @@
 
 /* globals document */
 
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import UIElement from '../../../src/view/uielement';
 import ViewContainerElement from '../../../src/view/containerelement';
 import ViewAttribtueElement from '../../../src/view/attributeelement';
@@ -17,8 +17,8 @@ import createViewRoot from '../_utils/createroot';
 import { setData as setViewData } from '../../../src/dev-utils/view';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
-describe( 'Document', () => {
-	let viewDocument, domRoot, domSelection, viewRoot, foo, bar, ui, ui2;
+describe( 'View', () => {
+	let view, viewDocument, domRoot, domSelection, viewRoot, foo, bar, ui, ui2;
 
 	function createUIElement( name, contents ) {
 		const element = new UIElement( name );
@@ -39,9 +39,10 @@ describe( 'Document', () => {
 		} );
 		document.body.appendChild( domRoot );
 
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		viewRoot = createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domRoot );
+		view.attachDomRoot( domRoot );
 
 		domSelection = document.getSelection();
 		domSelection.removeAllRanges();
@@ -55,15 +56,15 @@ describe( 'Document', () => {
 	} );
 
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 
 		domRoot.parentElement.removeChild( domRoot );
 	} );
 
 	function renderAndFireKeydownEvent( options ) {
-		viewDocument.render();
+		view.render();
 
-		const eventData = Object.assign( { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) }, options );
+		const eventData = Object.assign( { keyCode: keyCodes.arrowright, domTarget: view.domRoots.get( 'main' ) }, options );
 		viewDocument.fire( 'keydown', eventData );
 	}
 
@@ -89,7 +90,10 @@ describe( 'Document', () => {
 				// <container:p>foo<ui:span>xxx</ui:span>{}bar</container:p>
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] );
+
+				view.change( writer => {
+					writer.setSelection( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] );
+				} );
 
 				renderAndFireKeydownEvent( { keyCode: keyCodes.arrowleft } );
 
@@ -104,7 +108,10 @@ describe( 'Document', () => {
 				// <container:p>foo[]<ui:span>xxx</ui:span>bar</container:p>
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] );
+
+				view.change( writer => {
+					writer.setSelection( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -121,7 +128,10 @@ describe( 'Document', () => {
 				// <container:p>foo{}<ui:span>xxx</ui:span>bar</container:p>
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+
+				view.change( writer => {
+					writer.setSelection( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -138,7 +148,10 @@ describe( 'Document', () => {
 				// <container:p>foo{}<ui:span>xxx</ui:span><ui:span>yyy</ui:span>bar</container:p>'
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+
+				view.change( writer => {
+					writer.setSelection( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -155,9 +168,12 @@ describe( 'Document', () => {
 				// <container:p>foo{}<ui:span>xxx</ui:span><ui:span>yyy</ui:span></container:p><container:div></container:div>
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2 ] );
 				const div = new ViewContainerElement( 'div' );
-				viewRoot.appendChildren( p );
-				viewRoot.appendChildren( div );
-				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+
+				view.change( writer => {
+					viewRoot.appendChildren( p );
+					viewRoot.appendChildren( div );
+					writer.setSelection( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -175,7 +191,10 @@ describe( 'Document', () => {
 				const b = new ViewAttribtueElement( 'b', null, foo );
 				const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -193,7 +212,10 @@ describe( 'Document', () => {
 				const b = new ViewAttribtueElement( 'b', null, foo );
 				const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -221,7 +243,10 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -248,7 +273,10 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+				} );
 
 				renderAndFireKeydownEvent();
 
@@ -276,7 +304,10 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
+				} );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
@@ -290,42 +321,42 @@ describe( 'Document', () => {
 			} );
 
 			it( 'do nothing if selection is not directly before ui element', () => {
-				setViewData( viewDocument, '<container:p>fo{}o<ui:span></ui:span>bar</container:p>' );
+				setViewData( view, '<container:p>fo{}o<ui:span></ui:span>bar</container:p>' );
 				renderAndFireKeydownEvent();
 
 				check( 'foo', 2 );
 			} );
 
 			it( 'do nothing if selection is in attribute element but not before ui element', () => {
-				setViewData( viewDocument, '<container:p><attribute:b>foo{}</attribute:b>bar</container:p>' );
+				setViewData( view, '<container:p><attribute:b>foo{}</attribute:b>bar</container:p>' );
 				renderAndFireKeydownEvent();
 
 				check( 'foo', 3 );
 			} );
 
 			it( 'do nothing if selection is before non-empty attribute element', () => {
-				setViewData( viewDocument, '<container:p>fo{}<attribute:b>o</attribute:b><ui:span></ui:span>bar</container:p>' );
+				setViewData( view, '<container:p>fo{}<attribute:b>o</attribute:b><ui:span></ui:span>bar</container:p>' );
 				renderAndFireKeydownEvent();
 
 				check( 'fo', 2 );
 			} );
 
 			it( 'do nothing if selection is before container element - case 1', () => {
-				setViewData( viewDocument, '<container:p>foo{}</container:p><ui:span></ui:span><container:div>bar</container:div>' );
+				setViewData( view, '<container:p>foo{}</container:p><ui:span></ui:span><container:div>bar</container:div>' );
 				renderAndFireKeydownEvent();
 
 				check( 'foo', 3 );
 			} );
 
 			it( 'do nothing if selection is before container element - case 2', () => {
-				setViewData( viewDocument, '<container:div>foo{}<container:p></container:p><ui:span></ui:span></container:div>' );
+				setViewData( view, '<container:div>foo{}<container:p></container:p><ui:span></ui:span></container:div>' );
 				renderAndFireKeydownEvent();
 
 				check( 'foo', 3 );
 			} );
 
 			it( 'do nothing if selection is at the end of last container element', () => {
-				setViewData( viewDocument, '<container:p>foo{}</container:p>' );
+				setViewData( view, '<container:p>foo{}</container:p>' );
 				renderAndFireKeydownEvent();
 
 				check( 'foo', 3 );
@@ -334,14 +365,14 @@ describe( 'Document', () => {
 
 		describe( 'non-collapsed selection', () => {
 			it( 'should do nothing', () => {
-				setViewData( viewDocument, '<container:p>f{oo}<ui:span></ui:span>bar</container:p>' );
+				setViewData( view, '<container:p>f{oo}<ui:span></ui:span>bar</container:p>' );
 				renderAndFireKeydownEvent();
 
 				check( 'foo', 1, 'foo', 3 );
 			} );
 
 			it( 'should do nothing if selection is not before ui element - shift key pressed', () => {
-				setViewData( viewDocument, '<container:p>f{o}o<ui:span></ui:span>bar</container:p>' );
+				setViewData( view, '<container:p>f{o}o<ui:span></ui:span>bar</container:p>' );
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
 				check( 'foo', 1, 'foo', 2 );
@@ -352,7 +383,10 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
+				} );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
@@ -377,7 +411,10 @@ describe( 'Document', () => {
 				const i = new ViewAttribtueElement( 'i', null, b );
 				const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
+				} );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
@@ -403,7 +440,10 @@ describe( 'Document', () => {
 				const b2 = new ViewAttribtueElement( 'b' );
 				const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
+
+				view.change( writer => {
+					writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
+				} );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
@@ -417,14 +457,14 @@ describe( 'Document', () => {
 			} );
 		} );
 
-		it( 'should do nothing if dom position cannot be converted to view position', () => {
+		it( 'should do nothing if DOM position cannot be converted to view position', () => {
 			const newDiv = document.createElement( 'div' );
 			const domSelection = document.getSelection();
 
 			document.body.appendChild( newDiv );
 			domSelection.collapse( newDiv, 0 );
 
-			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) } );
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: view.domRoots.get( 'main' ) } );
 		} );
 	} );
 } );

+ 596 - 0
packages/ckeditor5-engine/tests/view/view/view.js

@@ -0,0 +1,596 @@
+/* globals document */
+
+import View from '../../../src/view/view';
+import MutationObserver from '../../../src/view/observer/mutationobserver';
+import count from '@ckeditor/ckeditor5-utils/src/count';
+import KeyObserver from '../../../src/view/observer/keyobserver';
+import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
+import SelectionObserver from '../../../src/view/observer/selectionobserver';
+import FocusObserver from '../../../src/view/observer/focusobserver';
+import createViewRoot from '../_utils/createroot';
+import Observer from '../../../src/view/observer/observer';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import ViewRange from '../../../src/view/range';
+import RootEditableElement from '../../../src/view/rooteditableelement';
+import ViewElement from '../../../src/view/element';
+import ViewPosition from '../../../src/view/position';
+import { isBlockFiller, BR_FILLER } from '../../../src/view/filler';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+describe( 'view', () => {
+	const DEFAULT_OBSERVERS_COUNT = 5;
+	let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		domRoot = createElement( document, 'div', {
+			id: 'editor',
+			contenteditable: 'true'
+		} );
+
+		document.body.appendChild( domRoot );
+
+		view = new View();
+		viewDocument = view.document;
+
+		ObserverMock = class extends Observer {
+			constructor( view ) {
+				super( view );
+
+				this.enable = sinon.spy();
+				this.disable = sinon.spy();
+				this.observe = sinon.spy();
+			}
+		};
+
+		instantiated = 0;
+		enabled = 0;
+
+		ObserverMockGlobalCount = class extends Observer {
+			constructor( view ) {
+				super( view );
+				instantiated++;
+
+				this.observe = sinon.spy();
+			}
+
+			enable() {
+				enabled++;
+			}
+		};
+	} );
+
+	afterEach( () => {
+		domRoot.remove();
+		view.destroy();
+	} );
+
+	it( 'should add default observers', () => {
+		expect( count( view._observers ) ).to.equal( DEFAULT_OBSERVERS_COUNT );
+		expect( view.getObserver( MutationObserver ) ).to.be.instanceof( MutationObserver );
+		expect( view.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
+		expect( view.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
+		expect( view.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
+		expect( view.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
+	} );
+
+	describe( 'attachDomRoot()', () => {
+		it( 'should attach DOM element to main view element', () => {
+			const domDiv = document.createElement( 'div' );
+			const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
+
+			expect( count( view.domRoots ) ).to.equal( 0 );
+
+			view.attachDomRoot( domDiv );
+
+			expect( count( view.domRoots ) ).to.equal( 1 );
+
+			expect( view.getDomRoot() ).to.equal( domDiv );
+			expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
+
+			expect( view._renderer.markedChildren.has( viewRoot ) ).to.be.true;
+			domDiv.remove();
+		} );
+
+		it( 'should attach DOM element to custom view element', () => {
+			const domH1 = document.createElement( 'h1' );
+			const viewH1 = createViewRoot( viewDocument, 'h1', 'header' );
+
+			expect( count( view.domRoots ) ).to.equal( 0 );
+
+			view.attachDomRoot( domH1, 'header' );
+
+			expect( count( view.domRoots ) ).to.equal( 1 );
+			expect( view.getDomRoot( 'header' ) ).to.equal( domH1 );
+			expect( view.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
+			expect( view._renderer.markedChildren.has( viewH1 ) ).to.be.true;
+		} );
+
+		it( 'should call observe on each observer', () => {
+			// The variable will be overwritten.
+			view.destroy();
+
+			view = new View();
+			viewDocument = view.document;
+			view._renderer.render = sinon.spy();
+
+			const domDiv1 = document.createElement( 'div' );
+			domDiv1.setAttribute( 'id', 'editor' );
+
+			const domDiv2 = document.createElement( 'div' );
+			domDiv2.setAttribute( 'id', 'editor' );
+
+			const observerMock = view.addObserver( ObserverMock );
+			const observerMockGlobalCount = view.addObserver( ObserverMockGlobalCount );
+
+			createViewRoot( viewDocument, 'div', 'root1' );
+			view.attachDomRoot( document.createElement( 'div' ), 'root1' );
+
+			sinon.assert.calledOnce( observerMock.observe );
+			sinon.assert.calledOnce( observerMockGlobalCount.observe );
+		} );
+	} );
+
+	describe( 'addObserver()', () => {
+		beforeEach( () => {
+			// The variable will be overwritten.
+			view.destroy();
+
+			view = new View();
+			viewDocument = view.document;
+			view._renderer.render = sinon.spy();
+		} );
+
+		afterEach( () => {
+			view.destroy();
+		} );
+
+		it( 'should be instantiated and enabled on adding', () => {
+			const observerMock = view.addObserver( ObserverMock );
+
+			expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
+
+			expect( observerMock ).to.have.property( 'document', viewDocument );
+			sinon.assert.calledOnce( observerMock.enable );
+		} );
+
+		it( 'should return observer instance each time addObserver is called', () => {
+			const observerMock1 = view.addObserver( ObserverMock );
+			const observerMock2 = view.addObserver( ObserverMock );
+
+			expect( observerMock1 ).to.be.instanceof( ObserverMock );
+			expect( observerMock2 ).to.be.instanceof( ObserverMock );
+			expect( observerMock1 ).to.equals( observerMock2 );
+		} );
+
+		it( 'should instantiate one observer only once', () => {
+			view.addObserver( ObserverMockGlobalCount );
+			view.addObserver( ObserverMockGlobalCount );
+
+			expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
+			expect( instantiated ).to.equal( 1 );
+			expect( enabled ).to.equal( 1 );
+
+			view.addObserver( ObserverMock );
+			expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
+		} );
+
+		it( 'should instantiate child class of already registered observer', () => {
+			class ObserverMock extends Observer {
+				enable() {}
+			}
+			class ChildObserverMock extends ObserverMock {
+				enable() {}
+			}
+
+			view.addObserver( ObserverMock );
+			view.addObserver( ChildObserverMock );
+
+			expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
+		} );
+
+		it( 'should be disabled and re-enabled on render', () => {
+			const observerMock = view.addObserver( ObserverMock );
+			view.render();
+
+			sinon.assert.calledOnce( observerMock.disable );
+			sinon.assert.calledOnce( view._renderer.render );
+			sinon.assert.calledTwice( observerMock.enable );
+		} );
+
+		it( 'should call observe on each root', () => {
+			createViewRoot( viewDocument, 'div', 'roo1' );
+			createViewRoot( viewDocument, 'div', 'roo2' );
+
+			view.attachDomRoot( document.createElement( 'div' ), 'roo1' );
+			view.attachDomRoot( document.createElement( 'div' ), 'roo2' );
+
+			const observerMock = view.addObserver( ObserverMock );
+
+			sinon.assert.calledTwice( observerMock.observe );
+		} );
+	} );
+
+	describe( 'getObserver()', () => {
+		it( 'should return observer it it is added', () => {
+			const addedObserverMock = view.addObserver( ObserverMock );
+			const getObserverMock = view.getObserver( ObserverMock );
+
+			expect( getObserverMock ).to.be.instanceof( ObserverMock );
+			expect( getObserverMock ).to.equal( addedObserverMock );
+		} );
+
+		it( 'should return undefined if observer is not added', () => {
+			const getObserverMock = view.getObserver( ObserverMock );
+
+			expect( getObserverMock ).to.be.undefined;
+		} );
+	} );
+
+	describe( 'scrollToTheSelection()', () => {
+		beforeEach( () => {
+			// Silence the Rect warnings.
+			testUtils.sinon.stub( log, 'warn' );
+		} );
+
+		it( 'does nothing when there are no ranges in the selection', () => {
+			const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
+
+			view.scrollToTheSelection();
+			sinon.assert.notCalled( stub );
+		} );
+
+		it( 'scrolls to the first range in selection with an offset', () => {
+			const root = createViewRoot( viewDocument, 'div', 'main' );
+			const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
+			const range = ViewRange.createIn( root );
+
+			view.attachDomRoot( domRoot );
+
+			// Make sure the window will have to scroll to the domRoot.
+			Object.assign( domRoot.style, {
+				position: 'absolute',
+				top: '-1000px',
+				left: '-1000px'
+			} );
+
+			view.change( writer => {
+				writer.setSelection( range );
+			} );
+
+			view.scrollToTheSelection();
+			sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
+		} );
+	} );
+
+	describe( 'disableObservers()', () => {
+		it( 'should disable observers', () => {
+			const addedObserverMock = view.addObserver( ObserverMock );
+
+			expect( addedObserverMock.enable.calledOnce ).to.be.true;
+			expect( addedObserverMock.disable.called ).to.be.false;
+
+			view.disableObservers();
+
+			expect( addedObserverMock.enable.calledOnce ).to.be.true;
+			expect( addedObserverMock.disable.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'enableObservers()', () => {
+		it( 'should enable observers', () => {
+			const addedObserverMock = view.addObserver( ObserverMock );
+
+			view.disableObservers();
+
+			expect( addedObserverMock.enable.calledOnce ).to.be.true;
+			expect( addedObserverMock.disable.calledOnce ).to.be.true;
+
+			view.enableObservers();
+
+			expect( addedObserverMock.enable.calledTwice ).to.be.true;
+			expect( addedObserverMock.disable.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'focus()', () => {
+		let domEditable, viewEditable;
+
+		beforeEach( () => {
+			domEditable = document.createElement( 'div' );
+			domEditable.setAttribute( 'contenteditable', 'true' );
+			document.body.appendChild( domEditable );
+			viewEditable = createViewRoot( viewDocument, 'div', 'main' );
+			view.attachDomRoot( domEditable );
+
+			view.change( writer => {
+				writer.setSelection( viewEditable, 0 );
+			} );
+		} );
+
+		afterEach( () => {
+			document.body.removeChild( domEditable );
+		} );
+
+		it( 'should focus editable with selection', () => {
+			const converterFocusSpy = testUtils.sinon.spy( view.domConverter, 'focus' );
+			const renderSpy = testUtils.sinon.spy( view, 'render' );
+
+			view.focus();
+
+			expect( converterFocusSpy.called ).to.be.true;
+			expect( renderSpy.calledOnce ).to.be.true;
+			expect( document.activeElement ).to.equal( domEditable );
+			const domSelection = document.getSelection();
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			const domRange = domSelection.getRangeAt( 0 );
+			expect( domRange.startContainer ).to.equal( domEditable );
+			expect( domRange.startOffset ).to.equal( 0 );
+			expect( domRange.collapsed ).to.be.true;
+		} );
+
+		it( 'should not focus if document is already focused', () => {
+			const converterFocusSpy = testUtils.sinon.spy( view.domConverter, 'focus' );
+			const renderSpy = testUtils.sinon.spy( view, 'render' );
+			viewDocument.isFocused = true;
+
+			view.focus();
+
+			expect( converterFocusSpy.called ).to.be.false;
+			expect( renderSpy.called ).to.be.false;
+		} );
+
+		it( 'should log warning when no selection', () => {
+			const logSpy = testUtils.sinon.stub( log, 'warn' );
+			view.change( writer => {
+				writer.setSelection( null );
+			} );
+
+			view.focus();
+			expect( logSpy.calledOnce ).to.be.true;
+			expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^view-focus-no-selection/ );
+		} );
+	} );
+
+	describe( 'isFocused', () => {
+		it( 'should change renderer.isFocused too', () => {
+			expect( viewDocument.isFocused ).to.equal( false );
+			expect( view._renderer.isFocused ).to.equal( false );
+
+			viewDocument.isFocused = true;
+
+			expect( viewDocument.isFocused ).to.equal( true );
+			expect( view._renderer.isFocused ).to.equal( true );
+		} );
+	} );
+
+	describe( 'render()', () => {
+		it( 'disable observers, renders and enable observers', () => {
+			const observerMock = view.addObserver( ObserverMock );
+			const renderStub = sinon.stub( view._renderer, 'render' );
+
+			view.render();
+
+			sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable );
+		} );
+	} );
+
+	describe( 'view and DOM integration', () => {
+		it( 'should remove content of the DOM', () => {
+			const domDiv = createElement( document, 'div', { id: 'editor' }, [
+				createElement( document, 'p' ),
+				createElement( document, 'p' )
+			] );
+
+			const view = new View();
+			const viewDocument = view.document;
+
+			createRoot( 'div', 'main', viewDocument );
+			view.attachDomRoot( domDiv );
+			view.render();
+
+			expect( domDiv.childNodes.length ).to.equal( 1 );
+			expect( isBlockFiller( domDiv.childNodes[ 0 ], BR_FILLER ) ).to.be.true;
+
+			view.destroy();
+			domDiv.remove();
+		} );
+
+		it( 'should render changes in the Document', () => {
+			const domDiv = document.createElement( 'div' );
+
+			const view = new View();
+			const viewDocument = view.document;
+			createRoot( 'div', 'main', viewDocument );
+			view.attachDomRoot( domDiv );
+
+			viewDocument.getRoot().appendChildren( new ViewElement( 'p' ) );
+			view.render();
+
+			expect( domDiv.childNodes.length ).to.equal( 1 );
+			expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );
+
+			view.destroy();
+		} );
+
+		it( 'should render attribute changes', () => {
+			const domRoot = document.createElement( 'div' );
+
+			const view = new View();
+			const viewDocument = view.document;
+			const viewRoot = createRoot( 'div', 'main', viewDocument );
+
+			view.attachDomRoot( domRoot );
+
+			const viewP = new ViewElement( 'p', { class: 'foo' } );
+			viewRoot.appendChildren( viewP );
+			view.render();
+
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' );
+
+			viewP._setAttribute( 'class', 'bar' );
+			view.render();
+
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' );
+
+			view.destroy();
+			domRoot.remove();
+		} );
+
+		describe( 'change()', () => {
+			it( 'should fire render event and it should trigger rendering on low priority', () => {
+				const renderSpy = sinon.spy( view._renderer, 'render' );
+				const beforeSpy = sinon.spy();
+				const afterSpy = sinon.spy();
+
+				view.on( 'render', beforeSpy );
+				view.on( 'render', afterSpy, { priority: 'low' } );
+
+				view.change( () => {} );
+
+				sinon.assert.callOrder( beforeSpy, renderSpy, afterSpy );
+			} );
+
+			it( 'should fire render event once for nested change blocks', () => {
+				const renderSpy = sinon.spy( view._renderer, 'render' );
+				const beforeSpy = sinon.spy();
+				const afterSpy = sinon.spy();
+
+				view.on( 'render', beforeSpy );
+				view.on( 'render', afterSpy, { priority: 'low' } );
+
+				view.change( () => {
+					view.change( () => {} );
+					view.change( () => {
+						view.change( () => {} );
+						view.change( () => {} );
+					} );
+					view.change( () => {} );
+				} );
+
+				sinon.assert.calledOnce( beforeSpy );
+				sinon.assert.calledOnce( renderSpy );
+				sinon.assert.calledOnce( afterSpy );
+				sinon.assert.callOrder( beforeSpy, renderSpy, afterSpy );
+			} );
+
+			it( 'should fire render event once even if render is called during the change', () => {
+				const renderSpy = sinon.spy( view._renderer, 'render' );
+				const beforeSpy = sinon.spy();
+				const afterSpy = sinon.spy();
+
+				view.on( 'render', beforeSpy );
+				view.on( 'render', afterSpy, { priority: 'low' } );
+
+				view.change( () => {
+					view.render();
+					view.change( () => {
+						view.render();
+					} );
+					view.render();
+				} );
+
+				sinon.assert.calledOnce( beforeSpy );
+				sinon.assert.calledOnce( renderSpy );
+				sinon.assert.calledOnce( afterSpy );
+				sinon.assert.callOrder( beforeSpy, renderSpy, afterSpy );
+			} );
+
+			it( 'should throw when someone tries to change view during rendering', () => {
+				const domDiv = document.createElement( 'div' );
+				const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
+				let renderingCalled = false;
+				view.attachDomRoot( domDiv );
+
+				view.change( writer => {
+					const p = writer.createContainerElement( 'p' );
+					const ui = writer.createUIElement( 'span', null, function( domDocument ) {
+						const element = this.toDomElement( domDocument );
+
+						expect( () => view.change( () => {} ) ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ );
+						renderingCalled = true;
+
+						return element;
+					} );
+					writer.insert( ViewPosition.createAt( p ), ui );
+					writer.insert( ViewPosition.createAt( viewRoot ), p );
+				} );
+
+				expect( renderingCalled ).to.be.true;
+				domDiv.remove();
+			} );
+
+			it( 'should throw when someone tries to call render() during rendering', () => {
+				const domDiv = document.createElement( 'div' );
+				const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
+				let renderingCalled = false;
+				view.attachDomRoot( domDiv );
+
+				view.change( writer => {
+					const p = writer.createContainerElement( 'p' );
+					const ui = writer.createUIElement( 'span', null, function( domDocument ) {
+						const element = this.toDomElement( domDocument );
+
+						expect( () => view.render() ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ );
+						renderingCalled = true;
+
+						return element;
+					} );
+					writer.insert( ViewPosition.createAt( p ), ui );
+					writer.insert( ViewPosition.createAt( viewRoot ), p );
+				} );
+
+				expect( renderingCalled ).to.be.true;
+				domDiv.remove();
+			} );
+
+			it( 'should throw when someone tries to call change() after rendering is finished but still in change block', () => {
+				view.on( 'render', () => {
+					expect( () => view.change( () => {} ) ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ );
+				}, { priority: 'low' } );
+
+				view.change( () => {} );
+			} );
+
+			it( 'should throw when someone tries to call render() after rendering is finished but still in change block', () => {
+				view.on( 'render', () => {
+					expect( () => view.render() ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ );
+				}, { priority: 'low' } );
+
+				view.change( () => {} );
+			} );
+
+			it( 'should NOT throw when someone tries to call change() before rendering', () => {
+				view.on( 'render', () => {
+					expect( () => view.change( () => {} ) ).not.to.throw( CKEditorError, /^applying-view-changes-on-rendering/ );
+				}, { priority: 'normal' } );
+
+				view.change( () => {} );
+			} );
+
+			it( 'should NOT throw when someone tries to call render() before rendering', () => {
+				view.on( 'render', () => {
+					expect( () => view.render() ).not.to.throw( CKEditorError, /^applying-view-changes-on-rendering/ );
+				}, { priority: 'normal' } );
+
+				view.change( () => {} );
+			} );
+		} );
+	} );
+
+	function createRoot( name, rootName, viewDoc ) {
+		const viewRoot = new RootEditableElement( name );
+
+		viewRoot.rootName = rootName;
+		viewRoot._document = viewDoc;
+		viewDoc.roots.add( viewRoot );
+
+		return viewRoot;
+	}
+} );

+ 18 - 11
packages/ckeditor5-engine/tests/view/writer/breakattributes.js

@@ -3,7 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
-import { breakAttributes } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
+import Document from '../../../src/view/document';
 import { stringify, parse } from '../../../src/dev-utils/view';
 import ContainerElement from '../../../src/view/containerelement';
 import AttributeElement from '../../../src/view/attributeelement';
@@ -13,8 +14,14 @@ import Range from '../../../src/view/range';
 import Position from '../../../src/view/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
-describe( 'writer', () => {
-	describe( 'breakAttributes', () => {
+describe( 'Writer', () => {
+	describe( 'breakAttributes()', () => {
+		let writer;
+
+		before( () => {
+			writer = new Writer( new Document() );
+		} );
+
 		describe( 'break position', () => {
 			/**
 			 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
@@ -26,7 +33,7 @@ describe( 'writer', () => {
 			function test( input, expected ) {
 				const { view, selection } = parse( input );
 
-				const newPosition = breakAttributes( selection.getFirstPosition() );
+				const newPosition = writer.breakAttributes( selection.getFirstPosition() );
 				expect( stringify( view.root, newPosition, {
 					showType: true,
 					showPriority: true
@@ -138,7 +145,7 @@ describe( 'writer', () => {
 			function test( input, expected ) {
 				const { view, selection } = parse( input );
 
-				const newRange = breakAttributes( selection.getFirstRange() );
+				const newRange = writer.breakAttributes( selection.getFirstRange() );
 				expect( stringify( view.root, newRange, { showType: true } ) ).to.equal( expected );
 			}
 
@@ -147,7 +154,7 @@ describe( 'writer', () => {
 				const p2 = new ContainerElement( 'p' );
 
 				expect( () => {
-					breakAttributes( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
+					writer.breakAttributes( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
 				} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
 			} );
 
@@ -155,7 +162,7 @@ describe( 'writer', () => {
 				const el = new AttributeElement( 'b' );
 
 				expect( () => {
-					breakAttributes( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
+					writer.breakAttributes( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
 				} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
 			} );
 
@@ -237,7 +244,7 @@ describe( 'writer', () => {
 				const position = new Position( img, 0 );
 
 				expect( () => {
-					breakAttributes( position );
+					writer.breakAttributes( position );
 				} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 			} );
 
@@ -248,7 +255,7 @@ describe( 'writer', () => {
 				const range = Range.createFromParentsAndOffsets( img, 0, b, 0 );
 
 				expect( () => {
-					breakAttributes( range );
+					writer.breakAttributes( range );
 				} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
 			} );
 
@@ -258,7 +265,7 @@ describe( 'writer', () => {
 				const position = new Position( span, 0 );
 
 				expect( () => {
-					breakAttributes( position );
+					writer.breakAttributes( position );
 				} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
 			} );
 
@@ -269,7 +276,7 @@ describe( 'writer', () => {
 				const range = Range.createFromParentsAndOffsets( span, 0, b, 0 );
 
 				expect( () => {
-					breakAttributes( range );
+					writer.breakAttributes( range );
 				} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
 			} );
 		} );

+ 22 - 17
packages/ckeditor5-engine/tests/view/writer/breakcontainer.js

@@ -3,28 +3,33 @@
  * For licensing, see LICENSE.md.
  */
 
-import { breakContainer } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
 import { stringify, parse } from '../../../src/dev-utils/view';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import ContainerElement from '../../../src/view/containerelement';
 import Position from '../../../src/view/position';
+import Document from '../../../src/view/document';
 
-describe( 'writer', () => {
-	/**
-	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
-	 * test break position.
-	 *
-	 * @param {String} input
-	 * @param {String} expected
-	 */
-	function test( input, expected ) {
-		const { view, selection } = parse( input );
+describe( 'Writer', () => {
+	describe( 'breakContainer()', () => {
+		let writer;
 
-		const newPosition = breakContainer( selection.getFirstPosition() );
-		expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
-	}
+		// Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+		// test break position.
+		//
+		// @param {String} input
+		// @param {String} expected
+		function test( input, expected ) {
+			const { view, selection } = parse( input );
+
+			const newPosition = writer.breakContainer( selection.getFirstPosition() );
+			expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
+		}
+
+		before( () => {
+			writer = new Writer( new Document() );
+		} );
 
-	describe( 'breakContainer', () => {
 		it( 'break inside element - should break container element at given position', () => {
 			test(
 				'<container:div>' +
@@ -62,7 +67,7 @@ describe( 'writer', () => {
 			const { selection } = parse( '<container:div>foo{}bar</container:div>' );
 
 			expect( () => {
-				breakContainer( selection.getFirstPosition() );
+				writer.breakContainer( selection.getFirstPosition() );
 			} ).to.throw( CKEditorError, /view-writer-break-non-container-element/ );
 		} );
 
@@ -71,7 +76,7 @@ describe( 'writer', () => {
 			const position = Position.createAt( element, 0 );
 
 			expect( () => {
-				breakContainer( position );
+				writer.breakContainer( position );
 			} ).to.throw( CKEditorError, /view-writer-break-root/ );
 		} );
 	} );

+ 22 - 15
packages/ckeditor5-engine/tests/view/writer/clear.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import { clear } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
 import Range from '../../../src/view/range';
 import { stringify, parse } from '../../../src/dev-utils/view';
 import ContainerElement from '../../../src/view/containerelement';
@@ -11,28 +11,35 @@ import AttributeElement from '../../../src/view/attributeelement';
 import EmptyElement from '../../../src/view/emptyelement';
 import UIElement from '../../../src/view/uielement';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import Document from '../../../src/view/document';
 
-describe( 'writer', () => {
-	// Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
-	//
-	// @param {Object} elementToRemove
-	// @param {String} input
-	// @param {String} expectedResult
-	function test( elementToRemove, input, expectedResult ) {
-		const { view, selection } = parse( input );
+describe( 'Writer', () => {
+	describe( 'clear()', () => {
+		let writer;
 
-		clear( selection.getFirstRange(), elementToRemove );
+		// Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
+		//
+		// @param {Object} elementToRemove
+		// @param {String} input
+		// @param {String} expectedResult
+		function test( elementToRemove, input, expectedResult ) {
+			const { view, selection } = parse( input );
 
-		expect( stringify( view, null, { showType: true } ) ).to.equal( expectedResult );
-	}
+			writer.clear( selection.getFirstRange(), elementToRemove );
+
+			expect( stringify( view, null, { showType: true } ) ).to.equal( expectedResult );
+		}
+
+		before( () => {
+			writer = new Writer( new Document() );
+		} );
 
-	describe( 'clear', () => {
 		it( 'should throw when range placed in two containers', () => {
 			const p1 = new ContainerElement( 'p' );
 			const p2 = new ContainerElement( 'p' );
 
 			expect( () => {
-				clear( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
+				writer.clear( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
 			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
 		} );
 
@@ -40,7 +47,7 @@ describe( 'writer', () => {
 			const el = new AttributeElement( 'b' );
 
 			expect( () => {
-				clear( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
+				writer.clear( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
 			} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
 		} );
 

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