Procházet zdrojové kódy

Merge branch 'master' into t/ckeditor5-dev/365

Piotrek Koszuliński před 8 roky
rodič
revize
2bc1a18252
100 změnil soubory, kde provedl 5315 přidání a 4123 odebrání
  1. 13 5
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 25 22
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  3. 418 0
      packages/ckeditor5-engine/src/conversion/conversion.js
  4. 268 306
      packages/ckeditor5-engine/src/conversion/downcast-converters.js
  5. 15 13
      packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js
  6. 83 49
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  7. 3 3
      packages/ckeditor5-engine/src/conversion/modelconsumable.js
  8. 0 400
      packages/ckeditor5-engine/src/conversion/two-way-converters.js
  9. 44 38
      packages/ckeditor5-engine/src/conversion/upcast-converters.js
  10. 38 48
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  11. 3 2
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  12. 30 17
      packages/ckeditor5-engine/src/dev-utils/model.js
  13. 22 17
      packages/ckeditor5-engine/src/dev-utils/view.js
  14. 10 15
      packages/ckeditor5-engine/src/model/document.js
  15. 31 8
      packages/ckeditor5-engine/src/model/documentselection.js
  16. 92 41
      packages/ckeditor5-engine/src/model/markercollection.js
  17. 74 2
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  18. 14 16
      packages/ckeditor5-engine/src/model/writer.js
  19. 14 3
      packages/ckeditor5-engine/src/view/attributeelement.js
  20. 3 3
      packages/ckeditor5-engine/src/view/containerelement.js
  21. 2 242
      packages/ckeditor5-engine/src/view/document.js
  22. 1 1
      packages/ckeditor5-engine/src/view/domconverter.js
  23. 19 8
      packages/ckeditor5-engine/src/view/editableelement.js
  24. 166 153
      packages/ckeditor5-engine/src/view/element.js
  25. 3 3
      packages/ckeditor5-engine/src/view/filler.js
  26. 5 5
      packages/ckeditor5-engine/src/view/observer/clickobserver.js
  27. 13 5
      packages/ckeditor5-engine/src/view/observer/domeventdata.js
  28. 3 3
      packages/ckeditor5-engine/src/view/observer/domeventobserver.js
  29. 6 6
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  30. 8 9
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  31. 5 7
      packages/ckeditor5-engine/src/view/observer/keyobserver.js
  32. 4 5
      packages/ckeditor5-engine/src/view/observer/mouseobserver.js
  33. 12 13
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  34. 14 6
      packages/ckeditor5-engine/src/view/observer/observer.js
  35. 10 20
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  36. 35 29
      packages/ckeditor5-engine/src/view/placeholder.js
  37. 1 1
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  38. 43 40
      packages/ckeditor5-engine/src/view/selection.js
  39. 3 3
      packages/ckeditor5-engine/src/view/uielement.js
  40. 415 0
      packages/ckeditor5-engine/src/view/view.js
  41. 0 8
      packages/ckeditor5-engine/src/view/view.jsdoc
  42. 1087 557
      packages/ckeditor5-engine/src/view/writer.js
  43. 2 2
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  44. 10 10
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  45. 495 0
      packages/ckeditor5-engine/tests/conversion/conversion.js
  46. 190 235
      packages/ckeditor5-engine/tests/conversion/downcast-converters.js
  47. 82 69
      packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js
  48. 51 37
      packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js
  49. 0 537
      packages/ckeditor5-engine/tests/conversion/two-way-converters.js
  50. 33 51
      packages/ckeditor5-engine/tests/conversion/upcast-converters.js
  51. 6 5
      packages/ckeditor5-engine/tests/conversion/upcast-selection-converters.js
  52. 28 26
      packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js
  53. 4 4
      packages/ckeditor5-engine/tests/conversion/viewconsumable.js
  54. 2 1
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  55. 28 22
      packages/ckeditor5-engine/tests/dev-utils/view.js
  56. 16 14
      packages/ckeditor5-engine/tests/manual/highlight.js
  57. 2 2
      packages/ckeditor5-engine/tests/manual/markers.js
  58. 5 7
      packages/ckeditor5-engine/tests/manual/nestededitable.js
  59. 5 4
      packages/ckeditor5-engine/tests/manual/placeholder.js
  60. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/880/1.js
  61. 9 8
      packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js
  62. 27 0
      packages/ckeditor5-engine/tests/model/documentselection.js
  63. 123 30
      packages/ckeditor5-engine/tests/model/markercollection.js
  64. 394 0
      packages/ckeditor5-engine/tests/model/utils/modifyselection.js
  65. 3 3
      packages/ckeditor5-engine/tests/model/writer.js
  66. 216 0
      packages/ckeditor5-engine/tests/tickets/1281.js
  67. 1 1
      packages/ckeditor5-engine/tests/view/_utils/createroot.js
  68. 4 4
      packages/ckeditor5-engine/tests/view/attributeelement.js
  69. 61 0
      packages/ckeditor5-engine/tests/view/document.js
  70. 0 414
      packages/ckeditor5-engine/tests/view/document/document.js
  71. 0 83
      packages/ckeditor5-engine/tests/view/document/integration.js
  72. 1 1
      packages/ckeditor5-engine/tests/view/domconverter/binding.js
  73. 1 2
      packages/ckeditor5-engine/tests/view/domconverter/domconverter.js
  74. 3 9
      packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js
  75. 15 38
      packages/ckeditor5-engine/tests/view/editableelement.js
  76. 98 98
      packages/ckeditor5-engine/tests/view/element.js
  77. 17 16
      packages/ckeditor5-engine/tests/view/manual/clickobserver.js
  78. 19 20
      packages/ckeditor5-engine/tests/view/manual/fakeselection.js
  79. 29 15
      packages/ckeditor5-engine/tests/view/manual/focus.js
  80. 21 10
      packages/ckeditor5-engine/tests/view/manual/focusobserver.js
  81. 16 6
      packages/ckeditor5-engine/tests/view/manual/immutable.js
  82. 16 11
      packages/ckeditor5-engine/tests/view/manual/inline-filler.js
  83. 15 6
      packages/ckeditor5-engine/tests/view/manual/keyobserver.js
  84. 10 8
      packages/ckeditor5-engine/tests/view/manual/mutationobserver.js
  85. 7 6
      packages/ckeditor5-engine/tests/view/manual/noselection-iframe.js
  86. 13 6
      packages/ckeditor5-engine/tests/view/manual/noselection.js
  87. 9 8
      packages/ckeditor5-engine/tests/view/manual/selectionobserver.js
  88. 16 21
      packages/ckeditor5-engine/tests/view/manual/uielement.js
  89. 11 9
      packages/ckeditor5-engine/tests/view/manual/x-index.js
  90. 5 5
      packages/ckeditor5-engine/tests/view/matcher.js
  91. 9 10
      packages/ckeditor5-engine/tests/view/node.js
  92. 6 5
      packages/ckeditor5-engine/tests/view/observer/clickobserver.js
  93. 9 7
      packages/ckeditor5-engine/tests/view/observer/domeventdata.js
  94. 25 24
      packages/ckeditor5-engine/tests/view/observer/domeventobserver.js
  95. 11 10
      packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js
  96. 28 23
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  97. 6 5
      packages/ckeditor5-engine/tests/view/observer/keyobserver.js
  98. 6 5
      packages/ckeditor5-engine/tests/view/observer/mouseobserver.js
  99. 36 34
      packages/ckeditor5-engine/tests/view/observer/mutationobserver.js
  100. 4 3
      packages/ckeditor5-engine/tests/view/observer/observer.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 { convertText, convertToModelFragment } from '../conversion/upcast-converters';
 
 
 import ViewDocumentFragment from '../view/documentfragment';
 import ViewDocumentFragment from '../view/documentfragment';
+import ViewDocument from '../view/document';
+import ViewWriter from '../view/writer';
 
 
 import ModelRange from '../model/range';
 import ModelRange from '../model/range';
 
 
@@ -74,7 +76,7 @@ export default class DataController {
 		 * @readonly
 		 * @readonly
 		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher}
 		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher}
 		 */
 		 */
-		this.downcastDispatcher = new DowncastDispatcher( this.model, {
+		this.downcastDispatcher = new DowncastDispatcher( {
 			mapper: this.mapper
 			mapper: this.mapper
 		} );
 		} );
 		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
 		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
@@ -85,7 +87,7 @@ export default class DataController {
 		 * @readonly
 		 * @readonly
 		 * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher}
 		 * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher}
 		 */
 		 */
-		this.upcastDispatcher = new UpcastDispatcher( this.model, {
+		this.upcastDispatcher = new UpcastDispatcher( {
 			schema: model.schema
 			schema: model.schema
 		} );
 		} );
 
 
@@ -143,9 +145,13 @@ export default class DataController {
 		const modelRange = ModelRange.createIn( modelElementOrFragment );
 		const modelRange = ModelRange.createIn( modelElementOrFragment );
 
 
 		const viewDocumentFragment = new ViewDocumentFragment();
 		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.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
 
 
-		this.downcastDispatcher.convertInsert( modelRange );
+		this.downcastDispatcher.convertInsert( modelRange, viewWriter );
 
 
 		if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
 		if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
 			// Then, if a document element is converted, convert markers.
 			// Then, if a document element is converted, convert markers.
@@ -153,7 +159,7 @@ export default class DataController {
 			const markers = _getMarkersRelativeToElement( modelElementOrFragment );
 			const markers = _getMarkersRelativeToElement( modelElementOrFragment );
 
 
 			for ( const [ name, range ] of markers ) {
 			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.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
 	 */
 	 */
 	toModel( viewElementOrFragment, context = '$root' ) {
 	toModel( viewElementOrFragment, context = '$root' ) {
-		return this.upcastDispatcher.convert( viewElementOrFragment, context );
+		return this.model.change( writer => {
+			return this.upcastDispatcher.convert( viewElementOrFragment, writer, context );
+		} );
 	}
 	}
 
 
 	/**
 	/**

+ 25 - 22
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -8,7 +8,7 @@
  */
  */
 
 
 import RootEditableElement from '../view/rooteditableelement';
 import RootEditableElement from '../view/rooteditableelement';
-import ViewDocument from '../view/document';
+import View from '../view/view';
 import Mapper from '../conversion/mapper';
 import Mapper from '../conversion/mapper';
 import DowncastDispatcher from '../conversion/downcastdispatcher';
 import DowncastDispatcher from '../conversion/downcastdispatcher';
 import {
 import {
@@ -49,12 +49,12 @@ export default class EditingController {
 		this.model = model;
 		this.model = model;
 
 
 		/**
 		/**
-		 * View document.
+		 * Editing view controller.
 		 *
 		 *
 		 * @readonly
 		 * @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.
 		 * Mapper which describes the model-view binding.
@@ -70,24 +70,23 @@ export default class EditingController {
 		 * @readonly
 		 * @readonly
 		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
 		 * @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 doc = this.model.document;
+		const selection = doc.selection;
+		const markers = this.model.markers;
 
 
 		this.listenTo( doc, 'change', () => {
 		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' } );
 		}, { priority: 'low' } );
 
 
 		// Convert selection from view to model.
 		// 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.
 		// Attach default model converters.
 		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
 		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
@@ -122,7 +121,9 @@ export default class EditingController {
 				if ( _operationAffectsMarker( operation, marker ) ) {
 				if ( _operationAffectsMarker( operation, marker ) ) {
 					// And if the operation in any way modifies the marker, remove the marker from the view.
 					// And if the operation in any way modifies the marker, remove the marker from the view.
 					removedMarkers.add( marker.name );
 					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.
 					// TODO: This stinks but this is the safest place to have this code.
 					this.model.document.differ.bufferMarkerChange( marker.name, markerRange, markerRange );
 					this.model.document.differ.bufferMarkerChange( marker.name, markerRange, markerRange );
@@ -130,12 +131,14 @@ export default class EditingController {
 			}
 			}
 		}, { priority: 'high' } );
 		}, { priority: 'high' } );
 
 
-		// If a marker is removed through `model.Model#markers` directly (not through operation), just remove it (if
-		// it was not removed already).
-		this.listenTo( model.markers, 'remove', ( evt, marker ) => {
-			if ( !removedMarkers.has( marker.name ) ) {
+		// If an existing marker is updated through `model.Model#markers` directly (not through operation), just remove it.
+		this.listenTo( model.markers, 'update', ( evt, marker, oldRange ) => {
+			if ( oldRange && !removedMarkers.has( marker.name ) ) {
 				removedMarkers.add( marker.name );
 				removedMarkers.add( marker.name );
-				this.downcastDispatcher.convertMarkerRemove( marker.name, marker.getRange() );
+
+				this.view.change( writer => {
+					this.downcastDispatcher.convertMarkerRemove( marker.name, oldRange, writer );
+				} );
 			}
 			}
 		} );
 		} );
 
 
@@ -147,7 +150,7 @@ export default class EditingController {
 		// Binds {@link module:engine/view/document~Document#roots view roots collection} to
 		// Binds {@link module:engine/view/document~Document#roots view roots collection} to
 		// {@link module:engine/model/document~Document#roots model roots collection} so creating
 		// {@link module:engine/model/document~Document#roots model roots collection} so creating
 		// model root automatically creates corresponding view root.
 		// 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.
 			// $graveyard is a special root that has no reflection in the view.
 			if ( root.rootName == '$graveyard' ) {
 			if ( root.rootName == '$graveyard' ) {
 				return null;
 				return null;
@@ -156,7 +159,7 @@ export default class EditingController {
 			const viewRoot = new RootEditableElement( root.name );
 			const viewRoot = new RootEditableElement( root.name );
 
 
 			viewRoot.rootName = root.rootName;
 			viewRoot.rootName = root.rootName;
-			viewRoot.document = this.view;
+			viewRoot._document = this.view.document;
 			this.mapper.bindElements( root, viewRoot );
 			this.mapper.bindElements( root, viewRoot );
 
 
 			return viewRoot;
 			return viewRoot;

+ 418 - 0
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -9,6 +9,18 @@
 
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 
+import {
+	downcastElementToElement,
+	downcastAttributeToElement,
+	downcastAttributeToAttribute
+} from './downcast-converters';
+
+import {
+	upcastElementToElement,
+	upcastElementToAttribute,
+	upcastAttributeToAttribute
+} from './upcast-converters';
+
 /**
 /**
  * An utility class that helps organizing dispatchers and adding converters to them.
  * An utility class that helps organizing dispatchers and adding converters to them.
  */
  */
@@ -102,6 +114,364 @@ export default class Conversion {
 		};
 		};
 	}
 	}
 
 
+	/**
+	 * Sets up converters between the model and the view which convert a model element to a view element (and vice versa).
+	 * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
+	 *
+	 *		// Simple conversion from `paragraph` model element to `<p>` view element (and vice versa).
+	 *		conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+	 *
+	 *		// Override other converters by specifying converter definition with higher priority.
+	 *		conversion.elementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
+	 *
+	 *		// View specified as an object instead of a string.
+	 *		conversion.elementToElement( {
+	 *			model: 'fancyParagraph',
+	 *			view: {
+	 *				name: 'p',
+	 *				class: 'fancy'
+	 *			}
+	 *		} );
+	 *
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `paragraph` element.
+	 *		conversion.elementToElement( {
+	 *			model: 'paragraph',
+	 *			view: 'p',
+	 *			upcastAlso: [
+	 *				'div',
+	 *				{
+	 *					// Any element with `display: block` style.
+	 *					style: {
+	 *						display: 'block'
+	 *					}
+	 *				}
+	 *			]
+	 *		} );
+	 *
+	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
+	 *		conversion.elementToElement( {
+	 *			model: 'heading',
+	 *			view: 'h2',
+	 *			// Convert "headling-like" paragraphs to headings.
+	 *			upcastAlso: viewElement => {
+	 *				const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *				if ( !fontSize ) {
+	 *					return null;
+	 *				}
+	 *
+	 *				const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *				if ( !match ) {
+	 *					return null;
+	 *				}
+	 *
+	 *				const size = Number( match[ 1 ] );
+	 *
+	 *				if ( size > 26 ) {
+	 *					// Returned value be an object with the matched properties.
+	 *					// Those properties will be "consumed" during conversion.
+	 *					// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *					return { name: true, style: [ 'font-size' ] };
+	 *				}
+	 *
+	 *				return null;
+	 *			}
+	 *		} );
+	 *
+	 * `definition.model` is a `String` with a model element name to converter from/to.
+	 * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
+	 *
+	 * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
+	 */
+	elementToElement( definition ) {
+		// Set up downcast converter.
+		this.for( 'downcast' ).add( downcastElementToElement( definition ) );
+
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastElementToElement( {
+					model,
+					view,
+					priority: definition.priority
+				} )
+			);
+		}
+	}
+
+	/**
+	 * Sets up converters between the model and the view which convert a model attribute to a view element (and vice versa).
+	 * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
+	 *
+	 *		// Simple conversion from `bold=true` attribute to `<strong>` view element (and vice versa).
+	 *		conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+	 *
+	 *		// Override other converters by specifying converter definition with higher priority.
+	 *		conversion.attributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
+	 *
+	 *		// View specified as an object instead of a string.
+	 *		conversion.attributeToElement( {
+	 *			model: 'bold',
+	 *			view: {
+	 *				name: 'span',
+	 *				class: 'bold'
+	 *			}
+	 *		} );
+	 *
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `bold` attribute.
+	 *		conversion.attributeToElement( {
+	 *			model: 'bold',
+	 *			view: 'strong',
+	 *			upcastAlso: [
+	 *				'b',
+	 *				{
+	 *					name: 'span',
+	 *					class: 'bold'
+	 *				},
+	 *				{
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-weight': 'bold'
+	 *					}
+	 *				},
+	 *				viewElement => {
+	 *					const fontWeight = viewElement.getStyle( 'font-weight' );
+	 *
+	 *					if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
+	 *						// Returned value be an object with the matched properties.
+	 *						// Those properties will be "consumed" during conversion.
+	 *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *						return {
+	 *							name: true,
+	 *							style: [ 'font-weight' ]
+	 *						};
+	 *					}
+	 *				}
+	 *			]
+	 *		} );
+	 *
+	 *		// Conversion from/to a model attribute key which value is an enum (`fontSize=big|small`).
+	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
+	 *		conversion.attributeToElement( {
+	 *			model: {
+	 *				key: 'fontSize',
+	 *				values: [ 'big', 'small' ]
+	 *			},
+	 *			view: {
+	 *				big: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '1.2em'
+	 *					}
+	 *				},
+	 *				small: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '0.8em'
+	 *					}
+	 *				}
+	 *			},
+	 *			upcastAlso: {
+	 *				big: viewElement => {
+	 *					const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *					if ( !fontSize ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *					if ( !match ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const size = Number( match[ 1 ] );
+	 *
+	 *					if ( viewElement.is( 'span' ) && size > 10 ) {
+	 *						// Returned value be an object with the matched properties.
+	 *						// Those properties will be "consumed" during conversion.
+	 *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *						return { name: true, style: [ 'font-size' ] };
+	 *					}
+	 *
+	 *					return null;
+	 *				},
+	 *				small: viewElement => {
+	 *					const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *					if ( !fontSize ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *					if ( !match ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const size = Number( match[ 1 ] );
+	 *
+	 *					if ( viewElement.is( 'span' ) && size < 10 ) {
+	 *						// Returned value be an object with the matched properties.
+	 *						// Those properties will be "consumed" during conversion.
+	 *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *						return { name: true, style: [ 'font-size' ] };
+	 *					}
+	 *
+	 *					return null;
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 * `definition.model` parameter specifies what model attribute should be converted from/to. It can be a `{ key, value }` object
+	 * describing attribute key and value to convert or a `String` specifying just attribute key (then `value` is set to `true`).
+	 * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
+	 *
+	 * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
+	 */
+	attributeToElement( definition ) {
+		// Set up downcast converter.
+		this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
+
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastElementToAttribute( {
+					view,
+					model,
+					priority: definition.priority
+				} )
+			);
+		}
+	}
+
+	/**
+	 * Sets up converters between the model and the view which convert a model attribute to a view attribute (and vice versa).
+	 * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute key and value).
+	 *
+	 *		// Simple conversion from `source` model attribute to `src` view attribute (and vice versa).
+	 *		conversion.attributeToAttribute( { model: 'source', view: 'src' } );
+	 *
+	 *		// Attributes values are strictly specified.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				name: 'image',
+	 *				key: 'aside',
+	 *				values: [ 'aside' ]
+	 *			},
+	 *			view: {
+	 *				aside: {
+	 *					name: 'img',
+	 *					key: 'class',
+	 *					value: [ 'aside', 'half-size' ]
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 *		// Set style attribute.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				name: 'image',
+	 *				key: 'aside',
+	 *				values: [ 'aside' ]
+	 *			},
+	 *			view: {
+	 *				aside: {
+	 *					name: 'img',
+	 *					key: 'style',
+	 *					value: {
+	 *						float: 'right',
+	 *						width: '50%',
+	 *						margin: '5px'
+	 *					}
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 *		// Conversion from/to a model attribute key which value is an enum (`align=right|center`).
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `align=right` attribute.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				key: 'align',
+	 *				values: [ 'right', 'center' ]
+	 *			},
+	 *			view: {
+	 *				right: {
+	 *					key: 'class',
+	 *					value: 'align-right'
+	 *				},
+	 *				center: {
+	 *					key: 'class',
+	 *					value: 'align-center'
+	 *				}
+	 *			},
+	 *			upcastAlso: {
+	 *				right: {
+	 *					style: {
+	 *						'text-align': 'right'
+	 *					}
+	 *				},
+	 *				center: {
+	 *					style: {
+	 *						'text-align': 'center'
+	 *					}
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 * `definition.model` parameter specifies what model attribute should be converted from/to.
+	 * It can be a `{ key, [ values ], [ name ] }` object or a `String`, which will be treated like `{ key: definition.model }`.
+	 * `key` property is the model attribute key to convert from/to.
+	 * `values` are the possible model attribute values. If `values` is not set, model attribute value will be the same as the
+	 * view attribute value.
+	 * If `name` is set, conversion will be set up only for model elements with the given name.
+	 *
+	 * `definition.view` parameter specifies what view attribute should be converted from/to.
+	 * It can be a `{ key, value, [ name ] }` object or a `String`, which will be treated like `{ key: definition.view }`.
+	 * `key` property is the view attribute key to convert from/to.
+	 * `value` is the view attribute value to convert from/to. If `definition.value` is not set, view attribute value will be
+	 * the same as the model attribute value.
+	 * If `key` is `'class'`, `value` can be a `String` or an array of `String`s.
+	 * If `key` is `'style'`, `value` is an object with key-value pairs.
+	 * In other cases, `value` is a `String`.
+	 * If `name` is set, conversion will be set up only for model elements with the given name.
+	 * If `definition.model.values` is set, `definition.view` is an object which assigns values from `definition.model.values`
+	 * to `{ key, value, [ name ] }` objects.
+	 *
+	 * `definition.upcastAlso` specifies which other matching view elements should be also upcast to given model configuration.
+	 * If `definition.model.values` is set, `definition.upcastAlso` should be an object assigning values from `definition.model.values`
+	 * to {@link module:engine/view/matcher~MatcherPattern}s or arrays of {@link module:engine/view/matcher~MatcherPattern}s.
+	 *
+	 * **Note:** `definition.model` and `definition.view` form should be mirrored, that is the same type of parameters should
+	 * be given in both parameters.
+	 *
+	 * @param {Object} definition Converter definition.
+	 * @param {String|Object} definition.model Model attribute to convert from/to.
+	 * @param {String|Object} definition.view View attribute to convert from/to.
+	 * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
+	 * Any view element matching `definition.upcastAlso` will also be converted to the given model attribute. `definition.upcastAlso`
+	 * is used only if `config.model.values` is specified.
+	 */
+	attributeToAttribute( definition ) {
+		// Set up downcast converter.
+		this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
+
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastAttributeToAttribute( {
+					view,
+					model
+				} )
+			);
+		}
+	}
+
 	/**
 	/**
 	 * Returns dispatchers registered under given group name.
 	 * Returns dispatchers registered under given group name.
 	 *
 	 *
@@ -129,6 +499,23 @@ export default class Conversion {
 	}
 	}
 }
 }
 
 
+/**
+ * Defines how the model should be converted from/to the view.
+ *
+ * @typedef {Object} module:engine/conversion/conversion~ConverterDefinition
+ *
+ * @property {*} [model] Model conversion definition. Describes model element or model attribute to convert. This parameter differs
+ * for different functions that accepts `ConverterDefinition`. See the description of a function to learn how to set it.
+ * @property {module:engine/view/elementdefinition~ElementDefinition|Object} view Definition of a view element to convert from/to.
+ * If `model` describes multiple values, `view` is an object that assigns those values (`view` object keys) to view element definitions
+ * (`view` object values).
+ * @property {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [upcastAlso]
+ * Any view element matching `upcastAlso` will also be converted to model. If `model` describes multiple values, `upcastAlso`
+ * is an object that assigns those values (`upcastAlso` object keys) to {@link module:engine/view/matcher~MatcherPattern}s
+ * (`upcastAlso` object values).
+ * @property {module:utils/priorities~PriorityString} [priority] Conversion priority.
+ */
+
 // Helper function for `Conversion` `.add()` method.
 // Helper function for `Conversion` `.add()` method.
 //
 //
 // Calls `conversionHelper` on each dispatcher from the group specified earlier in `.for()` call, effectively
 // Calls `conversionHelper` on each dispatcher from the group specified earlier in `.for()` call, effectively
@@ -143,3 +530,34 @@ function _addToDispatchers( dispatchers, conversionHelper ) {
 		conversionHelper( dispatcher );
 		conversionHelper( dispatcher );
 	}
 	}
 }
 }
+
+// Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
+// `definition.upcastAlso`.
+//
+// @param {module:engine/conversion/conversion~ConverterDefinition} definition
+// @returns {Array} Array containing view definitions.
+function* _getAllUpcastDefinitions( definition ) {
+	if ( definition.model.values ) {
+		for ( const value of definition.model.values ) {
+			const model = { key: definition.model.key, value };
+			const view = definition.view[ value ];
+			const upcastAlso = definition.upcastAlso ? definition.upcastAlso[ value ] : undefined;
+
+			yield* _getUpcastDefinition( model, view, upcastAlso );
+		}
+	} else {
+		yield* _getUpcastDefinition( definition.model, definition.view, definition.upcastAlso );
+	}
+}
+
+function* _getUpcastDefinition( model, view, upcastAlso ) {
+	yield { model, view };
+
+	if ( upcastAlso ) {
+		upcastAlso = Array.isArray( upcastAlso ) ? upcastAlso : [ upcastAlso ];
+
+		for ( const upcastAlsoItem of upcastAlso ) {
+			yield { model, view: upcastAlsoItem };
+		}
+	}
+}

+ 268 - 306
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -7,13 +7,8 @@ import ModelRange from '../model/range';
 import ModelSelection from '../model/selection';
 import ModelSelection from '../model/selection';
 import ModelElement from '../model/element';
 import ModelElement from '../model/element';
 
 
-import ViewContainerElement from '../view/containerelement';
-import ViewUIElement from '../view/uielement';
-import ViewElement from '../view/element';
 import ViewAttributeElement from '../view/attributeelement';
 import ViewAttributeElement from '../view/attributeelement';
-import ViewText from '../view/text';
 import ViewRange from '../view/range';
 import ViewRange from '../view/range';
-import viewWriter from '../view/writer';
 import DocumentSelection from '../model/documentselection';
 import DocumentSelection from '../model/documentselection';
 
 
 import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
 import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
@@ -31,9 +26,7 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *
  *		downcastElementToElement( { model: 'paragraph', view: 'p' } );
  *		downcastElementToElement( { model: 'paragraph', view: 'p' } );
  *
  *
- *		downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' );
- *
- *		downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } );
+ *		downcastElementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
  *
  *
  *		downcastElementToElement( {
  *		downcastElementToElement( {
  *			model: 'fancyParagraph',
  *			model: 'fancyParagraph',
@@ -45,27 +38,24 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *
  * 		downcastElementToElement( {
  * 		downcastElementToElement( {
  * 			model: 'heading',
  * 			model: 'heading',
- * 			view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+ * 			view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
  * 		} );
  * 		} );
  *
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  *
  * @param {Object} config Conversion configuration.
  * @param {Object} config Conversion configuration.
  * @param {String} config.model Name of the model element to convert.
  * @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 {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
+ * that takes model element and view writer as a parameters and returns a view container element.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastElementToElement( config, priority = 'normal' ) {
+export function downcastElementToElement( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
-	_normalizeToElementConfig( config, ViewContainerElement );
+	config.view = _normalizeToElementConfig( config.view, 'container' );
 
 
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority } );
+		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.priority || 'normal' } );
 	};
 	};
 }
 }
 
 
@@ -75,74 +65,74 @@ export function downcastElementToElement( config, priority = 'normal' ) {
  * This conversion results in wrapping view nodes in a view attribute element. For example, model text node with data
  * This conversion results in wrapping view nodes in a view attribute element. For example, model text node with data
  * `"Foo"` and `bold` attribute becomes `<strong>Foo</strong>` in the view.
  * `"Foo"` and `bold` attribute becomes `<strong>Foo</strong>` in the view.
  *
  *
- *		downcastAttributeToElement( 'bold', { view: 'strong' } );
- *
- *		downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' );
+ *		downcastAttributeToElement( { model: 'bold', view: 'strong' } );
  *
  *
- *		downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } );
+ *		downcastAttributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
  *
  *
- *		downcastAttributeToElement( 'bold', {
+ *		downcastAttributeToElement( {
+ *			model: 'invert',
  *			view: {
  *			view: {
  *				name: 'span',
  *				name: 'span',
- *				class: 'bold'
+ *				class: [ 'font-light', 'bg-dark' ]
  *			}
  *			}
  *		} );
  *		} );
  *
  *
- *		downcastAttributeToElement( 'styled', {
- *			model: 'dark',
+ *		downcastAttributeToElement( {
+ *			model: {
+ *				key: 'fontSize',
+ *				values: [ 'big', 'small' ]
+ *			},
  *			view: {
  *			view: {
- *				name: 'span',
- *				class: [ 'styled', 'styled-dark' ]
- *			}
- *		} );
- *
- *		downcastAttributeToElement( 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
+ *				big: {
  *					name: 'span',
  *					name: 'span',
  *					style: {
  *					style: {
  *						'font-size': '1.2em'
  *						'font-size': '1.2em'
  *					}
  *					}
- *				}
- *			},
- *			{
- *				model: 'small',
- *				view: {
+ *				},
+ *				small: {
  *					name: 'span',
  *					name: 'span',
  *					style: {
  *					style: {
  *						'font-size': '0.8em'
  *						'font-size': '0.8em'
  *					}
  *					}
  *				}
  *				}
  *			}
  *			}
- *		] );
+ *		} );
  *
  *
- * 		downcastAttributeToElement( 'bold', {
- * 			view: modelAttributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } )
+ * 		downcastAttributeToElement( {
+ * 			model: 'bold',
+ * 			view: ( modelAttributeValue, viewWriter ) => {
+ * 				return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
+ * 			}
  * 		} );
  * 		} );
  *
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  *
- * @param {String} modelAttributeKey The key of the attribute to convert.
- * @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.
- * The view element will be used then in conversion.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {Object} config Conversion configuration.
+ * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values }` object. `values` is an array
+ * of `String`s with possible values if the model attribute is enumerable.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view View element definition or a function
+ * that takes model attribute value and view writer as parameters and returns a view attribute element. If `config.model.values` is
+ * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastAttributeToElement( modelAttributeKey, config, priority = 'normal' ) {
+export function downcastAttributeToElement( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
-	_normalizeToElementConfig( config, ViewAttributeElement );
+	const modelKey = config.model.key ? config.model.key : config.model;
 
 
-	const elementCreator = _getCreatorForArrayConfig( config );
+	if ( config.model.values ) {
+		for ( const modelValue of config.model.values ) {
+			config.view[ modelValue ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
+		}
+	} else {
+		config.view = _normalizeToElementConfig( config.view, 'attribute' );
+	}
+
+	const elementCreator = _getFromAttributeCreator( config );
 
 
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( 'attribute:' + modelAttributeKey, wrap( elementCreator ), { priority } );
+		dispatcher.on( 'attribute:' + modelKey, wrap( elementCreator ), { priority: config.priority || 'normal' } );
 	};
 	};
 }
 }
 
 
@@ -152,74 +142,75 @@ export function downcastAttributeToElement( modelAttributeKey, config, priority
  * This conversion results in adding an attribute on a view node, basing on an attribute from a model node. For example,
  * This conversion results in adding an attribute on a view node, basing on an attribute from a model node. For example,
  * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
  * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
  *
  *
- *		downcastAttributeToAttribute( 'src' );
+ *		downcastAttributeToAttribute( { model: 'source', view: 'src' } );
  *
  *
- *		downcastAttributeToAttribute( 'source', { view: 'src' } );
+ *		downcastAttributeToAttribute( { model: 'source', view: 'href', priority: 'high' } );
  *
  *
- *		downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
- *
- *		downcastAttributeToAttribute( 'stylish', {
- *			view: {
- *				key: 'class',
- *				value: 'styled'
- *			}
+ *		downcastAttributeToAttribute( {
+ *			model: {
+ *				name: 'image',
+ *				key: 'source'
+ *			},
+ *			view: 'src'
  *		} );
  *		} );
  *
  *
- *		downcastAttributeToAttribute( 'styled', {
- *			model: 'dark',
+ *		downcastAttributeToAttribute( {
+ *			model: {
+ *				name: 'styled',
+ *				values: [ 'dark', 'light' ]
+ *			},
  *			view: {
  *			view: {
- *				key: 'class',
- *				value: 'styled styled-dark'
- *			}
- *		} );
- *
- *		downcastAttributeToAttribute( 'style', [
- *			{
- *				model: 'dark',
- *				view: {
+ *				dark: {
  *					key: 'class',
  *					key: 'class',
- *					value: 'styled-dark'
- *				}
- *			},
- *			{
- *				model: 'light',
- *				view: {
+ *					value: [ 'styled', 'styled-dark' ]
+ *				},
+ *				light: {
  *					key: 'class',
  *					key: 'class',
- *					value: 'styled-light'
+ *					value: [ 'styled', 'styled-light' ]
  *				}
  *				}
  *			}
  *			}
- *		] );
+ *		} );
  *
  *
- *		downcastAttributeToAttribute( 'style', {
- *			view: attributeValue => ( { key: 'class', value: 'style-' + attributeValue } )
+ *		downcastAttributeToAttribute( {
+ *			model: 'styled',
+ *			view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } )
  *		} );
  *		} );
  *
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  *
- * @param {String} modelAttributeKey The key of the attribute to convert.
- * @param {Object|Array.<Object>} [config] Conversion configuration. It is possible to provide multiple configurations in an array.
- * If not set, the conversion helper will assume 1-to-1 conversion, that is the view attribute key and view attribute value
- * will be same as model attribute key and model attribute value.
- * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined.
- * If `true` is provided, the configuration item will be used as a "default" configuration when no other item matches
- * the attribute value.
- * @param {String|Object|Function} [config.view] View attribute key, or an object with `key` and `value` properties (both `String`),
- * or a function that takes model attribute value and returns an object with `key` and `value` properties (both `String`).
- * If nothing is passed, the view attribute key and value will be equal to the model attribute key and value.
- * If a `String` is passed, it will be used as view attribute key and view attribute value will be equal to model attribute value.
- * If an object or a function returning an object is passed, its properties will be used to set view attribute key and value.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {Object} config Conversion configuration.
+ * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
+ * the attribute key, possible values and, optionally, an element name to convert from.
+ * @param {String|Object|Function} config.view View attribute key, or a `{ key, value }` object or a function that takes
+ * model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
+ * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
+ * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
+ * `{ key, value }` objects or a functions.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastAttributeToAttribute( modelAttributeKey, config = {}, priority = 'normal' ) {
+export function downcastAttributeToAttribute( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
-	_normalizeToAttributeConfig( modelAttributeKey, config );
+	const modelKey = config.model.key ? config.model.key : config.model;
+	let eventName = 'attribute:' + modelKey;
+
+	if ( config.model.name ) {
+		eventName += ':' + config.model.name;
+	}
+
+	if ( config.model.values ) {
+		for ( const modelValue of config.model.values ) {
+			config.view[ modelValue ] = _normalizeToAttributeConfig( config.view[ modelValue ] );
+		}
+	} else {
+		config.view = _normalizeToAttributeConfig( config.view );
+	}
 
 
-	const elementCreator = _getCreatorForArrayConfig( config );
+	const elementCreator = _getFromAttributeCreator( config );
 
 
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( 'attribute:' + modelAttributeKey, changeAttribute( elementCreator ), { priority } );
+		dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.priority || 'normal' } );
 	};
 	};
 }
 }
 
 
@@ -232,9 +223,7 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  *
  *		downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  *		downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
  *
  *
- *		downcastMarkerToElement( { model: 'search', view: 'marker-search' }, 'high' );
- *
- *		downcastMarkerToElement( { model: 'search', view: new ViewUIElement( 'span', { data-marker: 'search' } ) } );
+ *		downcastMarkerToElement( { model: 'search', view: 'search-result', priority: 'high' } );
  *
  *
  *		downcastMarkerToElement( {
  *		downcastMarkerToElement( {
  *			model: 'search',
  *			model: 'search',
@@ -248,14 +237,14 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  *
  * 		downcastMarkerToElement( {
  * 		downcastMarkerToElement( {
  * 			model: 'search',
  * 			model: 'search',
- * 			view: data => {
- *	 			return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+ * 			view: ( markerData, viewWriter ) => {
+ *	 			return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } );
  * 			}
  * 			}
  * 		} );
  * 		} );
  *
  *
  * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
  * 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}.
  * 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,
  * {@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
  * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
  * marker end boundary element.
  * marker end boundary element.
@@ -266,20 +255,19 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr
  *
  *
  * @param {Object} config Conversion configuration.
  * @param {Object} config Conversion configuration.
  * @param {String} config.model Name of the model marker (or model marker group) to convert.
  * @param {String} config.model Name of the model marker (or model marker group) to convert.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition
- * which will be used to build a view element for conversion or a function that takes model marker data as a parameter and
- * returns view element to use in conversion.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
+ * that takes model marker data as a parameter and returns view ui element.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastMarkerToElement( config, priority = 'normal' ) {
+export function downcastMarkerToElement( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
-	_normalizeToElementConfig( config, ViewUIElement );
+	config.view = _normalizeToElementConfig( config.view, 'ui' );
 
 
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority } );
-		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority } );
+		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.priority || 'normal' } );
 	};
 	};
 }
 }
 
 
@@ -304,7 +292,7 @@ export function downcastMarkerToElement( config, priority = 'normal' ) {
  *
  *
  *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
  *
  *
- *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
+ *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' }, priority: 'high' } );
  *
  *
  * 		downcastMarkerToHighlight( {
  * 		downcastMarkerToHighlight( {
  * 			model: 'comment',
  * 			model: 'comment',
@@ -319,9 +307,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
  * 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
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
+ * receives `data` object as parameter and should return a {@link module:engine/conversion/downcast-converters~HighlightDescriptor}.
+ * The `data` object properties 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.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  *
@@ -329,166 +316,124 @@ export function downcastMarkerToElement( config, priority = 'normal' ) {
  * @param {String} config.model Name of the model marker (or model marker group) to convert.
  * @param {String} config.model Name of the model marker (or model marker group) to convert.
  * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view Highlight descriptor
  * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view Highlight descriptor
  * which will be used for highlighting or a function that takes model marker data as a parameter and returns a highlight descriptor.
  * which will be used for highlighting or a function that takes model marker data as a parameter and returns a highlight descriptor.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function downcastMarkerToHighlight( config, priority = 'normal' ) {
+export function downcastMarkerToHighlight( config ) {
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority } );
-		dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority } );
-		dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority } );
+		dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.priority || 'normal' } );
 	};
 	};
 }
 }
 
 
-// Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
-// for generating a view element.
+// Takes `config.view`, and if it is a {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
+// to a function (because lower level converters accepts only element creator functions).
 //
 //
-// @param {Object} config Object with conversion helper configuration.
-// @param {Function} ViewElementClass View element class to use when generating view element from config.
-function _normalizeToElementConfig( config, ViewElementClass ) {
-	// If config is given as an array, normalize each entry separately.
-	if ( Array.isArray( config ) ) {
-		for ( const configEntry of config ) {
-			_normalizeToElementConfig( configEntry, ViewElementClass );
-		}
-
-		return;
+// @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
+// @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
+// @returns {Function} Element creator function to use in lower level converters.
+function _normalizeToElementConfig( view, viewElementType ) {
+	if ( typeof view == 'function' ) {
+		// If `view` is already a function, don't do anything.
+		return 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 ) ) {
-		// If `.view` is an object, use it to build view element instance.
-		config.view = _createViewElementFromDefinition( config.view, ViewElementClass );
-	}
-	// `.view` can be also a function or already a view element instance.
-	// These are already valid types which don't have to be normalized.
+	return ( modelData, viewWriter ) => _createViewElementFromDefinition( view, viewWriter, viewElementType );
 }
 }
 
 
 // Creates view element instance from provided viewElementDefinition and class.
 // Creates view element instance from provided viewElementDefinition and class.
 //
 //
 // @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
 // @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
-// @param {Function} ViewElementClass
+// @param {module:engine/view/writer~Writer} viewWriter
+// @param {'container'|'attribute'|'ui'} viewElementType
 // @returns {module:engine/view/element~Element}
 // @returns {module:engine/view/element~Element}
-function _createViewElementFromDefinition( viewElementDefinition, ViewElementClass ) {
-	const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+function _createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) {
+	if ( typeof viewElementDefinition == 'string' ) {
+		// If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
+		viewElementDefinition = { name: viewElementDefinition };
+	}
+
+	let element;
+
+	if ( viewElementType == 'container' ) {
+		element = viewWriter.createContainerElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	} else if ( viewElementType == 'attribute' ) {
+		element = viewWriter.createAttributeElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	} else {
+		// 'ui'.
+		element = viewWriter.createUIElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	}
 
 
 	if ( viewElementDefinition.style ) {
 	if ( viewElementDefinition.style ) {
-		element.setStyle( viewElementDefinition.style );
+		const keys = Object.keys( viewElementDefinition.style );
+
+		for ( const key of keys ) {
+			viewWriter.setStyle( key, viewElementDefinition.style[ key ], element );
+		}
 	}
 	}
 
 
 	if ( viewElementDefinition.class ) {
 	if ( viewElementDefinition.class ) {
 		const classes = viewElementDefinition.class;
 		const classes = viewElementDefinition.class;
 
 
 		if ( typeof classes == 'string' ) {
 		if ( typeof classes == 'string' ) {
-			element.addClass( classes );
+			viewWriter.addClass( classes, element );
 		} else {
 		} else {
-			element.addClass( ...classes );
+			for ( const className of classes ) {
+				viewWriter.addClass( className, element );
+			}
 		}
 		}
 	}
 	}
 
 
 	return element;
 	return element;
 }
 }
 
 
-// Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
-// for generating view attribute.
-//
-// @param {String} modelAttributeKey Model attribute key for which config is defined.
-// @param {Object} [config] Config with conversion helper configuration.
-function _normalizeToAttributeConfig( modelAttributeKey, config ) {
-	// If config is given as an array, normalize each entry separately.
-	if ( Array.isArray( config ) ) {
-		for ( const configEntry of config ) {
-			_normalizeToAttributeConfig( modelAttributeKey, configEntry );
-		}
+function _getFromAttributeCreator( config ) {
+	if ( config.model.values ) {
+		return ( modelAttributeValue, viewWriter ) => {
+			const view = config.view[ modelAttributeValue ];
 
 
-		return;
-	}
+			if ( view ) {
+				return view( modelAttributeValue, viewWriter );
+			}
 
 
-	// Build `.view` property.
-	// It is expected to be a creator function, that takes attribute value and model item and returns an object
-	// with `key` property and `value` property which are view attribute key and view attribute value.
-	if ( !config.view ) {
-		// If `.view` is not set, take both attribute name and attribute value from model.
-		const viewAttributeKey = modelAttributeKey;
-		config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } );
-	} else if ( typeof config.view == 'string' ) {
-		// If `.view` is set as a string, use it as a view attribute name. Value will be taken from model attribute value.
-		const viewAttributeKey = config.view;
-		config.view = modelAttributeValue => ( { key: viewAttributeKey, value: modelAttributeValue } );
-	} else if ( typeof config.view == 'object' ) {
-		// If `.view` is set as an object, use set key and value.
-		const viewAttributeKey = config.view.key;
-		const viewAttributeValue = config.view.value;
-		config.view = () => ( { key: viewAttributeKey, value: viewAttributeValue } );
+			return null;
+		};
+	} else {
+		return config.view;
 	}
 	}
-	// `.view` can be also already a function.
 }
 }
 
 
-// Takes config and creates a view element creator function that chooses an appropriate entry from the config depending on
-// the value of model attribute.
-//
-// Supports specifying config as a single object or an array of objects.
-// Supports `.view` defined as an object and as a function.
+// Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
+// for generating view attribute.
 //
 //
-// @param {Object|Array.<Object>} config Config with conversion helper configuration.
-function _getCreatorForArrayConfig( config ) {
-	if ( !Array.isArray( config ) ) {
-		config = [ config ];
+// @param {Object} view View configuration.
+function _normalizeToAttributeConfig( view ) {
+	if ( typeof view == 'string' ) {
+		return modelAttributeValue => ( { key: view, value: modelAttributeValue } );
+	} else if ( typeof view == 'object' ) {
+		return () => view;
+	} else {
+		// function.
+		return view;
 	}
 	}
-
-	// Get "default config" entry. It is the entry with `.model` property set to `true`.
-	// "Default" entry should be used if no other entry matched model attribute value.
-	const defaultConfig = config.find( configEntry => configEntry.model === undefined );
-
-	// Return a creator function.
-	return modelAttributeValue => {
-		// Set default config at first. It will be used if no other entry matches model attribute value.
-		let matchedConfigEntry = defaultConfig;
-
-		// Creator should check all entries from the config...
-		for ( const configEntry of config ) {
-			if ( configEntry.model === modelAttributeValue ) {
-				// If `.model` specified in entry matches converted attribute's value, choose it.
-				matchedConfigEntry = configEntry;
-				break;
-			}
-		}
-
-		// 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;
-		}
-
-		return null;
-	};
 }
 }
 
 
 /**
 /**
  * Function factory, creates a converter that converts node insertion changes from the model to the view.
  * 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}.
  * 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 result of the function will be inserted to the view.
  *
  *
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * 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.
  * {@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',
  *			'insert:myElem',
- *			insertElement( ( modelItem, consumable, conversionApi ) => {
- *				let myElem = new ViewElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, new ViewText( 'myText' ) );
+ *			insertElement( ( modelItem, viewWriter ) => {
+ *				const text = viewWriter.createText( 'myText' );
+ *				const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
  *
  *
  *				// Do something fancy with myElem using `modelItem` or other parameters.
  *				// Do something fancy with myElem using `modelItem` or other parameters.
  *
  *
@@ -496,28 +441,25 @@ 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.
  * @returns {Function} Insert element event converter.
  */
  */
 export function insertElement( elementCreator ) {
 export function insertElement( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.item, consumable, conversionApi );
+	return ( evt, data, conversionApi ) => {
+		const viewElement = elementCreator( data.item, conversionApi.writer );
 
 
 		if ( !viewElement ) {
 		if ( !viewElement ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( !consumable.consume( data.item, 'insert' ) ) {
+		if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
 			return;
 			return;
 		}
 		}
 
 
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 
 
 		conversionApi.mapper.bindElements( data.item, viewElement );
 		conversionApi.mapper.bindElements( data.item, viewElement );
-		viewWriter.insert( viewPosition, viewElement );
+		conversionApi.writer.insert( viewPosition, viewElement );
 	};
 	};
 }
 }
 
 
@@ -532,13 +474,14 @@ export function insertElement( elementCreator ) {
  * @returns {Function} Insert text event converter.
  * @returns {Function} Insert text event converter.
  */
  */
 export function insertText() {
 export function insertText() {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, 'insert' ) ) {
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
 			return;
 			return;
 		}
 		}
 
 
+		const viewWriter = conversionApi.writer;
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 		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 );
 		viewWriter.insert( viewPosition, viewText );
 	};
 	};
@@ -562,7 +505,7 @@ export function remove() {
 		const viewRange = new ViewRange( viewStart, viewEnd );
 		const viewRange = new ViewRange( viewStart, viewEnd );
 
 
 		// Trim the range to remove in case some UI elements are on the view range boundaries.
 		// 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.
 		// After the range is removed, unbind all view elements from the model.
 		// Range inside view document fragment is used to unbind deeply.
 		// Range inside view document fragment is used to unbind deeply.
@@ -586,21 +529,14 @@ export function remove() {
  * @returns {Function} Insert element event converter.
  * @returns {Function} Insert element event converter.
  */
  */
 export function insertUIElement( elementCreator ) {
 export function insertUIElement( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
+	return ( evt, data, conversionApi ) => {
 		// Create two view elements. One will be inserted at the beginning of marker, one at the end.
 		// Create two view elements. One will be inserted at the beginning of marker, one at the end.
 		// If marker is collapsed, only "opening" element will be inserted.
 		// If marker is collapsed, only "opening" element will be inserted.
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, conversionApi );
+		data.isOpening = true;
+		const viewStartElement = elementCreator( data, conversionApi.writer );
 
 
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, conversionApi );
-		}
+		data.isOpening = false;
+		const viewEndElement = elementCreator( data, conversionApi.writer );
 
 
 		if ( !viewStartElement || !viewEndElement ) {
 		if ( !viewStartElement || !viewEndElement ) {
 			return;
 			return;
@@ -611,18 +547,19 @@ export function insertUIElement( elementCreator ) {
 		// Marker that is collapsed has consumable build differently that non-collapsed one.
 		// Marker that is collapsed has consumable build differently that non-collapsed one.
 		// For more information see `addMarker` event description.
 		// For more information see `addMarker` event description.
 		// If marker's range is collapsed - check if it can be consumed.
 		// If marker's range is collapsed - check if it can be consumed.
-		if ( markerRange.isCollapsed && !consumable.consume( markerRange, evt.name ) ) {
+		if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) {
 			return;
 			return;
 		}
 		}
 
 
 		// If marker's range is not collapsed - consume all items inside.
 		// If marker's range is not collapsed - consume all items inside.
 		for ( const value of markerRange ) {
 		for ( const value of markerRange ) {
-			if ( !consumable.consume( value.item, evt.name ) ) {
+			if ( !conversionApi.consumable.consume( value.item, evt.name ) ) {
 				return;
 				return;
 			}
 			}
 		}
 		}
 
 
 		const mapper = conversionApi.mapper;
 		const mapper = conversionApi.mapper;
+		const viewWriter = conversionApi.writer;
 
 
 		// Add "opening" element.
 		// Add "opening" element.
 		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
 		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
@@ -646,26 +583,20 @@ export function insertUIElement( elementCreator ) {
  */
  */
 export function removeUIElement( elementCreator ) {
 export function removeUIElement( elementCreator ) {
 	return ( evt, data, conversionApi ) => {
 	return ( evt, data, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
 		// Create two view elements. One will be used to remove "opening element", the other for "closing element".
 		// Create two view elements. One will be used to remove "opening element", the other for "closing element".
 		// If marker was collapsed, only "opening" element will be removed.
 		// If marker was collapsed, only "opening" element will be removed.
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, conversionApi );
+		data.isOpening = true;
+		const viewStartElement = elementCreator( data, conversionApi.writer );
 
 
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, conversionApi );
-		}
+		data.isOpening = false;
+		const viewEndElement = elementCreator( data, conversionApi.writer );
 
 
 		if ( !viewStartElement || !viewEndElement ) {
 		if ( !viewStartElement || !viewEndElement ) {
 			return;
 			return;
 		}
 		}
 
 
 		const markerRange = data.markerRange;
 		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
 		// When removing the ui elements, we map the model range to view twice, because that view range
 		// may change after the first clearing.
 		// may change after the first clearing.
@@ -708,30 +639,60 @@ export function removeUIElement( elementCreator ) {
  *
  *
  * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
  * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
- * The function is passed all the parameters of the
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute} event.
+ * The function is passed model attribute value as first parameter and additional data about the change as a second parameter.
  * @returns {Function} Set/change attribute converter.
  * @returns {Function} Set/change attribute converter.
  */
  */
 export function changeAttribute( attributeCreator ) {
 export function changeAttribute( attributeCreator ) {
 	attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) );
 	attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) );
 
 
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, evt.name ) ) {
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
 			return;
 			return;
 		}
 		}
 
 
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
+		const viewWriter = conversionApi.writer;
+
 		// First remove the old attribute if there was one.
 		// First remove the old attribute if there was one.
-		const oldAttribute = attributeCreator( data.attributeOldValue, data, consumable, conversionApi );
+		const oldAttribute = attributeCreator( data.attributeOldValue, data );
 
 
 		if ( data.attributeOldValue !== null && oldAttribute ) {
 		if ( data.attributeOldValue !== null && oldAttribute ) {
-			conversionApi.mapper.toViewElement( data.item ).removeAttribute( oldAttribute.key );
+			if ( oldAttribute.key == 'class' ) {
+				const classes = Array.isArray( oldAttribute.value ) ? oldAttribute.value : [ oldAttribute.value ];
+
+				for ( const className of classes ) {
+					viewWriter.removeClass( className, viewElement );
+				}
+			} else if ( oldAttribute.key == 'style' ) {
+				const keys = Object.keys( oldAttribute.value );
+
+				for ( const key of keys ) {
+					viewWriter.removeStyle( key, viewElement );
+				}
+			} else {
+				viewWriter.removeAttribute( oldAttribute.key, viewElement );
+			}
 		}
 		}
 
 
 		// Then, if conversion was successful, set the new attribute.
 		// Then, if conversion was successful, set the new attribute.
-		const newAttribute = attributeCreator( data.attributeNewValue, data, consumable, conversionApi );
+		const newAttribute = attributeCreator( data.attributeNewValue, data );
 
 
 		if ( data.attributeNewValue !== null && newAttribute ) {
 		if ( data.attributeNewValue !== null && newAttribute ) {
-			conversionApi.mapper.toViewElement( data.item ).setAttribute( newAttribute.key, newAttribute.value );
+			if ( newAttribute.key == 'class' ) {
+				const classes = Array.isArray( newAttribute.value ) ? newAttribute.value : [ newAttribute.value ];
+
+				for ( const className of classes ) {
+					viewWriter.addClass( className, viewElement );
+				}
+			} else if ( newAttribute.key == 'style' ) {
+				const keys = Object.keys( newAttribute.value );
+
+				for ( const key of keys ) {
+					viewWriter.setStyle( key, newAttribute.value[ key ], viewElement );
+				}
+			} else {
+				viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
+			}
 		}
 		}
 	};
 	};
 }
 }
@@ -750,8 +711,7 @@ export function changeAttribute( attributeCreator ) {
  *			|- b {bold: true}                             |   |- ab
  *			|- b {bold: true}                             |   |- ab
  *			|- c                                          |- c
  *			|- 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}.
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute event}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * It's expected that the function returns a {@link module:engine/view/element~Element}.
  * The result of the function will be the wrapping element.
  * The result of the function will be the wrapping element.
@@ -760,36 +720,36 @@ export function changeAttribute( attributeCreator ) {
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * The converter automatically consumes corresponding value from consumables list, stops the event (see
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
  *
  *
- *		modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
+ *		modelDispatcher.on( 'attribute:bold', wrapItem( ( modelAttributeValue, viewWriter ) => {
+ *			return viewWriter.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.
  * @returns {Function} Set/change attribute converter.
  */
  */
 export function wrap( elementCreator ) {
 export function wrap( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
+	return ( evt, data, conversionApi ) => {
 		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
 		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
 		// or the attribute was removed.
 		// or the attribute was removed.
-		const oldViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeOldValue, data, consumable, conversionApi );
+		const oldViewElement = elementCreator( data.attributeOldValue, conversionApi.writer );
 
 
 		// Create node to wrap with.
 		// Create node to wrap with.
-		const newViewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeNewValue, data, consumable, conversionApi );
+		const newViewElement = elementCreator( data.attributeNewValue, conversionApi.writer );
 
 
 		if ( !oldViewElement && !newViewElement ) {
 		if ( !oldViewElement && !newViewElement ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( !consumable.consume( data.item, evt.name ) ) {
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
 			return;
 			return;
 		}
 		}
 
 
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
 			// Selection attribute conversion.
 			// Selection attribute conversion.
-			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), newViewElement, conversionApi.viewSelection );
+			viewWriter.wrap( viewSelection.getFirstRange(), newViewElement );
 		} else {
 		} else {
 			// Node attribute conversion.
 			// Node attribute conversion.
 			let viewRange = conversionApi.mapper.toViewRange( data.range );
 			let viewRange = conversionApi.mapper.toViewRange( data.range );
@@ -822,7 +782,7 @@ export function wrap( elementCreator ) {
  * @return {Function}
  * @return {Function}
  */
  */
 export function highlightText( highlightDescriptor ) {
 export function highlightText( highlightDescriptor ) {
-	return ( evt, data, consumable, conversionApi ) => {
+	return ( evt, data, conversionApi ) => {
 		if ( data.markerRange.isCollapsed ) {
 		if ( data.markerRange.isCollapsed ) {
 			return;
 			return;
 		}
 		}
@@ -837,14 +797,16 @@ export function highlightText( highlightDescriptor ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( !consumable.consume( data.item, evt.name ) ) {
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
 			return;
 			return;
 		}
 		}
 
 
 		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
 		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
 
 
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
-			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), viewElement, conversionApi.viewSelection );
+			viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection );
 		} else {
 		} else {
 			const viewRange = conversionApi.mapper.toViewRange( data.range );
 			const viewRange = conversionApi.mapper.toViewRange( data.range );
 			viewWriter.wrap( viewRange, viewElement );
 			viewWriter.wrap( viewRange, viewElement );
@@ -856,7 +818,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.
  * 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
  * 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.
  * 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.
  * When `addHighlight` custom property is not present, element is not converted in any special way.
@@ -870,7 +832,7 @@ export function highlightText( highlightDescriptor ) {
  * @return {Function}
  * @return {Function}
  */
  */
 export function highlightElement( highlightDescriptor ) {
 export function highlightElement( highlightDescriptor ) {
-	return ( evt, data, consumable, conversionApi ) => {
+	return ( evt, data, conversionApi ) => {
 		if ( data.markerRange.isCollapsed ) {
 		if ( data.markerRange.isCollapsed ) {
 			return;
 			return;
 		}
 		}
@@ -885,7 +847,7 @@ export function highlightElement( highlightDescriptor ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( !consumable.test( data.item, evt.name ) ) {
+		if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
 			return;
 			return;
 		}
 		}
 
 
@@ -893,14 +855,14 @@ export function highlightElement( highlightDescriptor ) {
 
 
 		if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
 		if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
 			// Consume element itself.
 			// Consume element itself.
-			consumable.consume( data.item, evt.name );
+			conversionApi.consumable.consume( data.item, evt.name );
 
 
 			// Consume all children nodes.
 			// Consume all children nodes.
 			for ( const value of ModelRange.createIn( data.item ) ) {
 			for ( const value of ModelRange.createIn( data.item ) ) {
-				consumable.consume( value.item, evt.name );
+				conversionApi.consumable.consume( value.item, evt.name );
 			}
 			}
 
 
-			viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor );
+			viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer );
 		}
 		}
 	};
 	};
 }
 }
@@ -914,7 +876,7 @@ export function highlightElement( highlightDescriptor ) {
  * highlight descriptor. See {link module:engine/conversion/downcast-converters~highlightDescriptorToAttributeElement}.
  * highlight descriptor. See {link module:engine/conversion/downcast-converters~highlightDescriptorToAttributeElement}.
  *
  *
  * For elements, the converter checks if an element has `removeHighlight` function stored as
  * 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.
  * In such case, children of that element will not be converted.
  *
  *
  * When `removeHighlight` is not present, element is not converted in any special way.
  * When `removeHighlight` is not present, element is not converted in any special way.
@@ -948,7 +910,7 @@ export function removeHighlight( highlightDescriptor ) {
 		// First, iterate through all items and remove highlight from those container elements that have custom highlight handling.
 		// First, iterate through all items and remove highlight from those container elements that have custom highlight handling.
 		for ( const item of items ) {
 		for ( const item of items ) {
 			if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) {
 			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.
 				// If container element had custom handling, remove all it's children from further processing.
 				for ( const descendant of ViewRange.createIn( item ) ) {
 				for ( const descendant of ViewRange.createIn( item ) ) {
@@ -960,6 +922,7 @@ export function removeHighlight( highlightDescriptor ) {
 		// Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end
 		// 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).
 		// to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged).
 		const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
 		const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
+		const viewWriter = conversionApi.writer;
 
 
 		for ( const item of Array.from( items ).reverse() ) {
 		for ( const item of Array.from( items ).reverse() ) {
 			if ( item.is( 'textProxy' ) ) {
 			if ( item.is( 'textProxy' ) ) {
@@ -1005,15 +968,14 @@ export function createViewElementFromHighlightDescriptor( descriptor ) {
 	const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
 	const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
 
 
 	if ( descriptor.class ) {
 	if ( descriptor.class ) {
-		const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ];
-		viewElement.addClass( ...cssClasses );
+		viewElement._addClass( descriptor.class );
 	}
 	}
 
 
 	if ( descriptor.priority ) {
 	if ( descriptor.priority ) {
-		viewElement.priority = descriptor.priority;
+		viewElement._priority = descriptor.priority;
 	}
 	}
 
 
-	viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id );
+	viewElement._setCustomProperty( 'highlightDescriptorId', descriptor.id );
 
 
 	return viewElement;
 	return viewElement;
 }
 }

+ 15 - 13
packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-import viewWriter from '../view/writer';
-
 /**
 /**
  * Contains {@link module:engine/model/selection~Selection model selection} to
  * Contains {@link module:engine/model/selection~Selection model selection} to
  * {@link module:engine/view/selection~Selection view selection} converters for
  * {@link module:engine/view/selection~Selection view selection} converters for
@@ -23,14 +21,14 @@ import viewWriter from '../view/writer';
  * @returns {Function} Selection converter.
  * @returns {Function} Selection converter.
  */
  */
 export function convertRangeSelection() {
 export function convertRangeSelection() {
-	return ( evt, data, consumable, conversionApi ) => {
+	return ( evt, data, conversionApi ) => {
 		const selection = data.selection;
 		const selection = data.selection;
 
 
 		if ( selection.isCollapsed ) {
 		if ( selection.isCollapsed ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( !consumable.consume( selection, 'selection' ) ) {
+		if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
 			return;
 			return;
 		}
 		}
 
 
@@ -41,7 +39,7 @@ export function convertRangeSelection() {
 			viewRanges.push( viewRange );
 			viewRanges.push( viewRange );
 		}
 		}
 
 
-		conversionApi.viewSelection.setTo( viewRanges, selection.isBackward );
+		conversionApi.writer.setSelection( viewRanges, selection.isBackward );
 	};
 	};
 }
 }
 
 
@@ -68,22 +66,23 @@ export function convertRangeSelection() {
  * @returns {Function} Selection converter.
  * @returns {Function} Selection converter.
  */
  */
 export function convertCollapsedSelection() {
 export function convertCollapsedSelection() {
-	return ( evt, data, consumable, conversionApi ) => {
+	return ( evt, data, conversionApi ) => {
 		const selection = data.selection;
 		const selection = data.selection;
 
 
 		if ( !selection.isCollapsed ) {
 		if ( !selection.isCollapsed ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( !consumable.consume( selection, 'selection' ) ) {
+		if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
 			return;
 			return;
 		}
 		}
 
 
+		const viewWriter = conversionApi.writer;
 		const modelPosition = selection.getFirstPosition();
 		const modelPosition = selection.getFirstPosition();
 		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
 		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
 		const brokenPosition = viewWriter.breakAttributes( viewPosition );
 		const brokenPosition = viewWriter.breakAttributes( viewPosition );
 
 
-		conversionApi.viewSelection.setTo( brokenPosition );
+		viewWriter.setSelection( brokenPosition );
 	};
 	};
 }
 }
 
 
@@ -112,17 +111,20 @@ export function convertCollapsedSelection() {
  * @returns {Function} Selection converter.
  * @returns {Function} Selection converter.
  */
  */
 export function clearAttributes() {
 export function clearAttributes() {
-	return ( evt, data, consumable, conversionApi ) => {
-		for ( const range of conversionApi.viewSelection.getRanges() ) {
+	return ( evt, data, conversionApi ) => {
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
+		for ( const range of viewSelection.getRanges() ) {
 			// Not collapsed selection should not have artifacts.
 			// Not collapsed selection should not have artifacts.
 			if ( range.isCollapsed ) {
 			if ( range.isCollapsed ) {
 				// Position might be in the node removed by the view writer.
 				// Position might be in the node removed by the view writer.
 				if ( range.end.parent.document ) {
 				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.
  * {@link module:engine/model/selection~Selection model selection} conversion.
  */
  */
 export function clearFakeSelection() {
 export function clearFakeSelection() {
-	return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection.setFake( false );
+	return ( evt, data, conversionApi ) => conversionApi.writer.setFakeSelection( false );
 }
 }

+ 83 - 49
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -73,12 +73,15 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
  * {@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).
  * 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`:
  * Example of a custom converter for `DowncastDispatcher`:
  *
  *
  *		// We will convert inserting "paragraph" model element into the model.
  *		// We will convert inserting "paragraph" model element into the model.
- *		downcastDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
+ *		downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
  *			// Remember to check whether the change has not been consumed yet and consume it.
  *			// Remember to check whether the change has not been consumed yet and consume it.
- *			if ( consumable.consume( data.item, 'insert' ) ) {
+ *			if ( conversionApi.consumable.consume( data.item, 'insert' ) ) {
  *				return;
  *				return;
  *			}
  *			}
  *
  *
@@ -86,13 +89,13 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  *			const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  *			const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  *
  *
  *			// Create <p> element that will be inserted in view at `viewPosition`.
  *			// 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.
  *			// Bind the newly created view element to model element so positions will map accordingly in future.
  *			conversionApi.mapper.bindElements( data.item, viewElement );
  *			conversionApi.mapper.bindElements( data.item, viewElement );
  *
  *
  *			// Add the newly created view element to the view.
  *			// Add the newly created view element to the view.
- *			viewWriter.insert( viewPosition, viewElement );
+ *			conversionApi.writer.insert( viewPosition, viewElement );
  *
  *
  *			// Remember to stop the event propagation.
  *			// Remember to stop the event propagation.
  *			evt.stop();
  *			evt.stop();
@@ -102,18 +105,9 @@ export default class DowncastDispatcher {
 	/**
 	/**
 	 * Creates a `DowncastDispatcher` instance.
 	 * Creates a `DowncastDispatcher` instance.
 	 *
 	 *
-	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls.
 	 * @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.
 		 * 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.
 	 * 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/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.
 		// Convert changes that happened on model tree.
 		for ( const entry of differ.getChanges() ) {
 		for ( const entry of differ.getChanges() ) {
 			if ( entry.type == 'insert' ) {
 			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' ) {
 			} else if ( entry.type == 'remove' ) {
-				this.convertRemove( entry.position, entry.length, entry.name );
+				this.convertRemove( entry.position, entry.length, entry.name, writer );
 			} else {
 			} else {
 				// entry.type == 'attribute'.
 				// 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.
 		// After the view is updated, convert markers which has changed.
 		for ( const change of differ.getMarkersToAdd() ) {
 		for ( const change of differ.getMarkersToAdd() ) {
-			this.convertMarkerAdd( change.name, change.range );
+			this.convertMarkerAdd( change.name, change.range, writer );
 		}
 		}
 	}
 	}
 
 
@@ -155,10 +150,13 @@ export default class DowncastDispatcher {
 	 * @fires insert
 	 * @fires insert
 	 * @fires attribute
 	 * @fires attribute
 	 * @param {module:engine/model/range~Range} range Inserted range.
 	 * @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.
 		// Create a list of things that can be consumed, consisting of nodes and their attributes.
-		const consumable = this._createInsertConsumable( range );
+		this.conversionApi.consumable = this._createInsertConsumable( range );
 
 
 		// Fire a separate insert event for each node and text fragment contained in the range.
 		// Fire a separate insert event for each node and text fragment contained in the range.
 		for ( const value of range ) {
 		for ( const value of range ) {
@@ -169,7 +167,7 @@ export default class DowncastDispatcher {
 				range: itemRange
 				range: itemRange
 			};
 			};
 
 
-			this._testAndFire( 'insert', data, consumable );
+			this._testAndFire( 'insert', data );
 
 
 			// Fire a separate addAttribute event for each attribute that was set on inserted items.
 			// Fire a separate addAttribute event for each attribute that was set on inserted items.
 			// This is important because most attributes converters will listen only to add/change/removeAttribute events.
 			// This is important because most attributes converters will listen only to add/change/removeAttribute events.
@@ -179,9 +177,11 @@ export default class DowncastDispatcher {
 				data.attributeOldValue = null;
 				data.attributeOldValue = null;
 				data.attributeNewValue = item.getAttribute( key );
 				data.attributeNewValue = item.getAttribute( key );
 
 
-				this._testAndFire( `attribute:${ key }`, data, consumable );
+				this._testAndFire( `attribute:${ key }`, data );
 			}
 			}
 		}
 		}
+
+		this._clearConversionApi();
 	}
 	}
 
 
 	/**
 	/**
@@ -190,9 +190,14 @@ export default class DowncastDispatcher {
 	 * @param {module:engine/model/position~Position} position Position from which node was removed.
 	 * @param {module:engine/model/position~Position} position Position from which node was removed.
 	 * @param {Number} length Offset size of removed node.
 	 * @param {Number} length Offset size of removed node.
 	 * @param {String} name Name 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 );
 		this.fire( 'remove:' + name, { position, length }, this.conversionApi );
+
+		this._clearConversionApi();
 	}
 	}
 
 
 	/**
 	/**
@@ -205,10 +210,13 @@ export default class DowncastDispatcher {
 	 * @param {String} key Key of the attribute that has changed.
 	 * @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 {*} 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 {*} 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.
 		// Create a list with attributes to consume.
-		const consumable = this._createConsumableForRange( range, `attribute:${ key }` );
+		this.conversionApi.consumable = this._createConsumableForRange( range, `attribute:${ key }` );
 
 
 		// Create a separate attribute event for each node in the range.
 		// Create a separate attribute event for each node in the range.
 		for ( const value of range ) {
 		for ( const value of range ) {
@@ -222,8 +230,10 @@ export default class DowncastDispatcher {
 				attributeNewValue: newValue
 				attributeNewValue: newValue
 			};
 			};
 
 
-			this._testAndFire( `attribute:${ key }`, data, consumable );
+			this._testAndFire( `attribute:${ key }`, data );
 		}
 		}
+
+		this._clearConversionApi();
 	}
 	}
 
 
 	/**
 	/**
@@ -235,18 +245,22 @@ export default class DowncastDispatcher {
 	 * @fires addMarker
 	 * @fires addMarker
 	 * @fires attribute
 	 * @fires attribute
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
+	 * @param {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 ) {
+		const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
 
 
-		this.fire( 'selection', { selection }, consumable, this.conversionApi );
+		this.conversionApi.writer = writer;
+		this.conversionApi.consumable = this._createSelectionConsumable( selection, markersAtSelection );
+
+		this.fire( 'selection', { selection }, this.conversionApi );
 
 
 		if ( !selection.isCollapsed ) {
 		if ( !selection.isCollapsed ) {
 			return;
 			return;
 		}
 		}
 
 
-		for ( const marker of markers ) {
+		for ( const marker of markersAtSelection ) {
 			const markerRange = marker.getRange();
 			const markerRange = marker.getRange();
 
 
 			if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
 			if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
@@ -259,8 +273,8 @@ export default class DowncastDispatcher {
 				markerRange
 				markerRange
 			};
 			};
 
 
-			if ( consumable.test( selection, 'addMarker:' + marker.name ) ) {
-				this.fire( 'addMarker:' + marker.name, data, consumable, this.conversionApi );
+			if ( this.conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
+				this.fire( 'addMarker:' + marker.name, data, this.conversionApi );
 			}
 			}
 		}
 		}
 
 
@@ -274,10 +288,12 @@ export default class DowncastDispatcher {
 			};
 			};
 
 
 			// Do not fire event if the attribute has been consumed.
 			// Do not fire event if the attribute has been consumed.
-			if ( consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
-				this.fire( 'attribute:' + data.attributeKey, data, consumable, this.conversionApi );
+			if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
+				this.fire( 'attribute:' + data.attributeKey, data, this.conversionApi );
 			}
 			}
 		}
 		}
+
+		this._clearConversionApi();
 	}
 	}
 
 
 	/**
 	/**
@@ -287,13 +303,16 @@ export default class DowncastDispatcher {
 	 * @fires addMarker
 	 * @fires addMarker
 	 * @param {String} markerName Marker name.
 	 * @param {String} markerName Marker name.
 	 * @param {module:engine/model/range~Range} markerRange Marker range.
 	 * @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).
 		// 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' ) {
 		if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
 			return;
 			return;
 		}
 		}
 
 
+		this.conversionApi.writer = writer;
+
 		// In markers' case, event name == consumable name.
 		// In markers' case, event name == consumable name.
 		const eventName = 'addMarker:' + markerName;
 		const eventName = 'addMarker:' + markerName;
 
 
@@ -302,28 +321,29 @@ export default class DowncastDispatcher {
 			const consumable = new Consumable();
 			const consumable = new Consumable();
 			consumable.add( markerRange, eventName );
 			consumable.add( markerRange, eventName );
 
 
-			this.fire( eventName, {
-				markerName,
-				markerRange
-			}, consumable, this.conversionApi );
+			this.conversionApi.consumable = consumable;
+
+			this.fire( eventName, { markerName, markerRange }, this.conversionApi );
 
 
 			return;
 			return;
 		}
 		}
 
 
 		// Create consumable for each item in range.
 		// Create consumable for each item in range.
-		const consumable = this._createConsumableForRange( markerRange, eventName );
+		this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
 
 
 		// Create separate event for each node in the range.
 		// Create separate event for each node in the range.
 		for ( const item of markerRange.getItems() ) {
 		for ( const item of markerRange.getItems() ) {
 			// Do not fire event for already consumed items.
 			// Do not fire event for already consumed items.
-			if ( !consumable.test( item, eventName ) ) {
+			if ( !this.conversionApi.consumable.test( item, eventName ) ) {
 				continue;
 				continue;
 			}
 			}
 
 
 			const data = { item, range: Range.createOn( item ), markerName, markerRange };
 			const data = { item, range: Range.createOn( item ), markerName, markerRange };
 
 
-			this.fire( eventName, data, consumable, this.conversionApi );
+			this.fire( eventName, data, this.conversionApi );
 		}
 		}
+
+		this._clearConversionApi();
 	}
 	}
 
 
 	/**
 	/**
@@ -332,14 +352,19 @@ export default class DowncastDispatcher {
 	 * @fires removeMarker
 	 * @fires removeMarker
 	 * @param {String} markerName Marker name.
 	 * @param {String} markerName Marker name.
 	 * @param {module:engine/model/range~Range} markerRange Marker range.
 	 * @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).
 		// 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' ) {
 		if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
 			return;
 			return;
 		}
 		}
 
 
+		this.conversionApi.writer = writer;
+
 		this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
 		this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
+
+		this._clearConversionApi();
 	}
 	}
 
 
 	/**
 	/**
@@ -416,17 +441,26 @@ export default class DowncastDispatcher {
 	 * @fires attribute
 	 * @fires attribute
 	 * @param {String} type Event type.
 	 * @param {String} type Event type.
 	 * @param {Object} data Event data.
 	 * @param {Object} data Event data.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 */
 	 */
-	_testAndFire( type, data, consumable ) {
-		if ( !consumable.test( data.item, type ) ) {
+	_testAndFire( type, data ) {
+		if ( !this.conversionApi.consumable.test( data.item, type ) ) {
 			// Do not fire event if the item was consumed.
 			// Do not fire event if the item was consumed.
 			return;
 			return;
 		}
 		}
 
 
 		const name = data.item.name || '$text';
 		const name = data.item.name || '$text';
 
 
-		this.fire( type + ':' + name, data, consumable, this.conversionApi );
+		this.fire( type + ':' + name, data, this.conversionApi );
+	}
+
+	/**
+	 * Clears conversion API object.
+	 *
+	 * @private
+	 */
+	_clearConversionApi() {
+		delete this.conversionApi.writer;
+		delete this.conversionApi.consumable;
 	}
 	}
 
 
 	/**
 	/**

+ 3 - 3
packages/ckeditor5-engine/src/conversion/modelconsumable.js

@@ -53,9 +53,9 @@ import TextProxy from '../model/textproxy';
  *		//   ├─ <img />
  *		//   ├─ <img />
  *		//   └─ <caption>
  *		//   └─ <caption>
  *		//       └─ foo
  *		//       └─ foo
- *		modelConversionDispatcher.on( 'insert:image', ( evt, data, consumable, conversionApi ) => {
+ *		modelConversionDispatcher.on( 'insert:image', ( evt, data, conversionApi ) => {
  *			// First, consume the `image` element.
  *			// First, consume the `image` element.
- *			consumable.consume( data.item, 'insert' );
+ *			conversionApi.consumable.consume( data.item, 'insert' );
  *
  *
  *			// Just create normal image element for the view.
  *			// Just create normal image element for the view.
  *			// Maybe it will be "decorated" later.
  *			// Maybe it will be "decorated" later.
@@ -69,7 +69,7 @@ import TextProxy from '../model/textproxy';
  *				// `modelCaption` insertion change is consumed from consumable values.
  *				// `modelCaption` insertion change is consumed from consumable values.
  *				// It will not be converted by other converters, but it's children (probably some text) will be.
  *				// It will not be converted by other converters, but it's children (probably some text) will be.
  *				// Through mapping, converters for text will know where to insert contents of `modelCaption`.
  *				// Through mapping, converters for text will know where to insert contents of `modelCaption`.
- *				if ( consumable.consume( modelCaption, 'insert' ) ) {
+ *				if ( conversionApi.consumable.consume( modelCaption, 'insert' ) ) {
  *					const viewCaption = new ViewElement( 'figcaption' );
  *					const viewCaption = new ViewElement( 'figcaption' );
  *
  *
  *					const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] );
  *					const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] );

+ 0 - 400
packages/ckeditor5-engine/src/conversion/two-way-converters.js

@@ -1,400 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/two-way-converters
- */
-
-import {
-	downcastElementToElement,
-	downcastAttributeToElement,
-	downcastAttributeToAttribute
-} from './downcast-converters';
-
-import {
-	upcastElementToElement,
-	upcastElementToAttribute,
-	upcastAttributeToAttribute
-} from './upcast-converters';
-
-/**
- * Defines a conversion between the model and the view where a model element is represented as a view element (and vice versa).
- * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
- *
- *		elementToElement( conversion, { model: 'paragraph', view: 'p' } );
- *
- *		elementToElement( conversion, {
- *			model: 'fancyParagraph',
- *			view: {
- *				name: 'p',
- *				class: 'fancy'
- *			}
- *		} );
- *
- *		elementToElement( conversion, {
- *			model: 'paragraph',
- *			view: 'p',
- *			upcastAlso: [
- *				'div',
- *				{
- *					// Match any name.
- *					name: /./,
- *					style: {
- *						display: 'block'
- *					}
- *				}
- *			]
- *		} );
- *
- *		elementToElement( conversion, {
- *			model: 'heading',
- *			view: 'h2',
- *			// Convert "headling-like" paragraphs to headings.
- *			upcastAlso: viewElement => {
- *				const fontSize = viewElement.getStyle( 'font-size' );
- *
- *				if ( !fontSize ) {
- *					return null;
- *				}
- *
- *				const match = fontSize.match( /(\d+)\s*px/ );
- *
- *				if ( !match ) {
- *					return null;
- *				}
- *
- *				const size = Number( match[ 1 ] );
- *
- *				if ( size > 26 ) {
- *					return { name: true, style: [ 'font-size' ] };
- *				}
- *
- *				return null;
- *			}
- *		} );
- *
- * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
- * @param {Object} definition Conversion definition.
- * @param {String} definition.model Name of the model element to convert.
- * @param {module:engine/view/elementdefinition~ElementDefinition} definition.view Definition of a view element to convert from/to.
- * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
- * Any view element matching `upcastAlso` will also be converted to the given model element.
- */
-export function elementToElement( conversion, definition ) {
-	// Set up downcast converter.
-	conversion.for( 'downcast' ).add( downcastElementToElement( definition ) );
-
-	// Set up upcast converter.
-	for ( const view of _getAllViews( definition ) ) {
-		const priority = view == definition.view ? 'normal' : 'high';
-
-		conversion.for( 'upcast' ).add( upcastElementToElement( {
-			model: definition.model,
-			view
-		}, priority ) );
-	}
-}
-
-/**
- * Defines a conversion between the model and the view where a model attribute is represented as a view element (and vice versa).
- * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
- *
- *		attributeToElement( conversion, 'bold', { view: 'strong' } );
- *
- *		attributeToElement( conversion, 'bold', {
- *			view: {
- *				name: 'span',
- *				class: 'bold'
- *			}
- *		} );
- *
- *		attributeToElement( conversion, 'bold', {
- *			view: 'strong',
- *			upcastAlso: [
- *				'b',
- *				{
- *					name: 'span',
- *					class: 'bold'
- *				},
- *				{
- *					name: 'span',
- *					style: {
- *						'font-weight': 'bold'
- *					}
- *				},
- *				viewElement => {
- *					const fontWeight = viewElement.getStyle( 'font-weight' );
- *
- *					if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
- *						return {
- *							name: true,
- *							style: [ 'font-weight' ]
- *						};
- *					}
- *				}
- *			]
- *		} );
- *
- *		attributeToElement( conversion, 'styled', {
- *			model: 'dark',
- *			view: {
- *				name: 'span',
- *				class: [ 'styled', 'styled-dark' ]
- *			}
- *		} );
- *
- *		attributeToElement( conversion, 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '1.2em'
- *					}
- *				}
- *			},
- *			{
- *				model: 'small',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '0.8em'
- *					}
- *				}
- *			}
- *		] );
- *
- *		attributeToElement( conversion, 'fontSize', [
- *			{
- *				model: 'big',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '1.2em'
- *					}
- *				},
- *				upcastAlso: viewElement => {
- *					const fontSize = viewElement.getStyle( 'font-size' );
- *
- *					if ( !fontSize ) {
- *						return null;
- *					}
- *
- *					const match = fontSize.match( /(\d+)\s*px/ );
- *
- *					if ( !match ) {
- *						return null;
- *					}
- *
- *					const size = Number( match[ 1 ] );
- *
- *					if ( viewElement.is( 'span' ) && size > 10 ) {
- *						return { name: true, style: [ 'font-size' ] };
- *					}
- *
- *					return null;
- *				}
- *			},
- *			{
- *				model: 'small',
- *				view: {
- *					name: 'span',
- *					style: {
- *						'font-size': '0.8em'
- *					}
- *				},
- *				upcastAlso: viewElement => {
- *					const fontSize = viewElement.getStyle( 'font-size' );
- *
- *					if ( !fontSize ) {
- *						return null;
- *					}
- *
- *					const match = fontSize.match( /(\d+)\s*px/ );
- *
- *					if ( !match ) {
- *						return null;
- *					}
- *
- *					const size = Number( match[ 1 ] );
- *
- *					if ( viewElement.is( 'span' ) && size < 10 ) {
- *						return { name: true, style: [ 'font-size' ] };
- *					}
- *
- *					return null;
- *				}
- *			}
- *		] );
- *
- * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
- * @param {String} modelAttributeKey The key of the model attribute to convert.
- * @param {Object|Array.<Object>} definition Conversion definition. It is possible to provide multiple definitions in an array.
- * @param {*} [definition.model] The value of the converted model attribute. If omitted, when downcasted, the item will be treated
- * as a default item, that will be used when no other item matches. When upcasted, the model attribute value will be set to `true`.
- * @param {module:engine/view/elementdefinition~ElementDefinition} definition.view Definition of a view element to convert from/to.
- * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
- * Any view element matching `upcastAlso` will also be converted to the given model element.
- */
-export function attributeToElement( conversion, modelAttributeKey, definition ) {
-	// Set downcast (model to view conversion).
-	conversion.for( 'downcast' ).add( downcastAttributeToElement( modelAttributeKey, definition ) );
-
-	// Set upcast (view to model conversion). In this case, we need to re-organise the definition config.
-	if ( !Array.isArray( definition ) ) {
-		definition = [ definition ];
-	}
-
-	for ( const item of definition ) {
-		const model = _getModelAttributeDefinition( modelAttributeKey, item.model );
-
-		for ( const view of _getAllViews( item ) ) {
-			const priority = view == item.view ? 'normal' : 'high';
-
-			conversion.for( 'upcast' ).add( upcastElementToAttribute( {
-				view,
-				model
-			}, priority ) );
-		}
-	}
-}
-
-/**
- * Defines a conversion between the model and the view where a model attribute is represented as a view attribute (and vice versa).
- * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute name and value).
- *
- *		attributeToAttribute( conversion, 'src' );
- *
- *		attributeToAttribute( conversion, 'source', { view: 'src' } );
- *
- *		attributeToAttribute( conversion, 'aside', {
- *			model: true,
- *			view: {
- *				name: 'img',
- *				key: 'class',
- *				value: 'aside half-size'
- *			}
- *		} );
- *
- *		attributeToAttribute( conversion, 'styled', [
- *			{
- *				model: 'dark',
- *				view: {
- *					key: 'class',
- *					value: 'styled styled-dark'
- *				}
- *			},
- *			{
- *				model: 'light',
- *				view: {
- *					key: 'class',
- *					value: 'styled styled-light'
- *				}
- *			}
- *		] );
- *
- *		attributeToAttribute( conversion, 'align', [
- *			{
- *				model: 'right',
- *				view: {
- *					key: 'class',
- *					value: 'align-right'
- *				},
- *				upcastAlso: viewElement => {
- *					if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
- *						return {
- *							style: [ 'text-align' ]
- *						};
- *					}
- *
- *					return null;
- *				}
- *			},
- *			{
- *				model: 'center',
- *				view: {
- *					key: 'class',
- *					value: 'align-center'
- *				},
- *				upcastAlso: {
- *					style: {
- *						'text-align': 'center'
- *					}
- *				}
- *			}
- *		] );
- *
- * @param {module:engine/conversion/conversion~Conversion} conversion Conversion class instance with registered conversion dispatchers.
- * @param {String} modelAttributeKey The key of the model attribute to convert.
- * @param {Object|Array.<Object>} [definition] Conversion definition. It is possible to provide multiple definitions in an array.
- * If not set, the conversion helper will assume 1-to-1 conversion, that is the model attribute key and value will be same
- * as the view attribute key and value.
- * @param {*} [definition.model] The value of the converted model attribute. If omitted, when downcasting,
- * the item will be treated as a default item, that will be used when no other item matches. When upcasting conversion,
- * the model attribute value will be set to the same value as in the view.
- * @param {Object} definition.view View attribute conversion details. Given object has required `key` property,
- * specifying view attribute key, optional `value` property, specifying view attribute value and optional `name`
- * property specifying a view element name from/on which the attribute should be converted. If `value` is not given,
- * the view attribute value will be equal to model attribute value.
- * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
- * Any view element matching `upcastAlso` will also be converted to the given model element.
- */
-export function attributeToAttribute( conversion, modelAttributeKey, definition ) {
-	// Set up downcast converter.
-	conversion.for( 'downcast' ).add( downcastAttributeToAttribute( modelAttributeKey, definition ) );
-
-	// Set up upcast converter. In this case, we need to re-organise the definition config.
-	if ( !definition ) {
-		definition = { view: modelAttributeKey };
-	}
-
-	if ( !Array.isArray( definition ) ) {
-		definition = [ definition ];
-	}
-
-	for ( const item of definition ) {
-		const model = _getModelAttributeDefinition( modelAttributeKey, item.model );
-
-		for ( const view of _getAllViews( item ) ) {
-			const priority = view == item.view ? 'low' : 'normal';
-
-			conversion.for( 'upcast' ).add( upcastAttributeToAttribute( {
-				view,
-				model
-			}, priority ) );
-		}
-	}
-}
-
-// Helper function, normalizes input data into a correct config form that can be accepted by conversion helpers. The
-// correct form is either `String` or an object with `key` and `value` properties.
-//
-// @param {String} key Model attribute key.
-// @param {*} [model] Model attribute value.
-// @returns {Object} Normalized model attribute definition.
-function _getModelAttributeDefinition( key, model ) {
-	if ( model === undefined ) {
-		return key;
-	} else {
-		return {
-			key, value: model
-		};
-	}
-}
-
-// Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
-// `definition.upcastAlso`.
-//
-// @param {Object} definition Conversion definition.
-// @returns {Array} Array containing view definitions.
-function _getAllViews( definition ) {
-	if ( !definition.upcastAlso ) {
-		return [ definition.view ];
-	} else {
-		const upcastAlso = Array.isArray( definition.upcastAlso ) ? definition.upcastAlso : [ definition.upcastAlso ];
-
-		return [ definition.view ].concat( upcastAlso );
-	}
-}

+ 44 - 38
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -26,7 +26,7 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *
  *
  *		upcastElementToElement( { view: 'p', model: 'paragraph' } );
  *		upcastElementToElement( { view: 'p', model: 'paragraph' } );
  *
  *
- *		upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
+ *		upcastElementToElement( { view: 'p', model: 'paragraph', priority: 'high' } );
  *
  *
  *		upcastElementToElement( {
  *		upcastElementToElement( {
  *			view: {
  *			view: {
@@ -37,19 +37,13 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  *		} );
  *		} );
  *
  *
  *		upcastElementToElement( {
  *		upcastElementToElement( {
- *			view: {
- *				name: 'p',
- *				class: 'fancy'
- *			},
- *			model: new ModelElement( 'p', { fancy: true } )
- *		} );
- *
- *		upcastElementToElement( {
  * 			view: {
  * 			view: {
  *				name: 'p',
  *				name: 'p',
  *				class: 'heading'
  *				class: 'heading'
  * 			},
  * 			},
- * 			model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
+ * 			model: ( viewElement, modelWriter ) => {
+ * 				return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
+ * 			}
  * 		} );
  * 		} );
  *
  *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
@@ -58,10 +52,10 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
  * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
  * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
  * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
  * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function upcastElementToElement( config, priority = 'normal' ) {
+export function upcastElementToElement( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
 	const converter = _prepareToElementConverter( config );
 	const converter = _prepareToElementConverter( config );
@@ -70,7 +64,7 @@ export function upcastElementToElement( config, priority = 'normal' ) {
 	const eventName = elementName ? 'element:' + elementName : 'element';
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority } );
+		dispatcher.on( eventName, converter, { priority: config.priority || 'normal' } );
 	};
 	};
 }
 }
 
 
@@ -84,7 +78,7 @@ export function upcastElementToElement( config, priority = 'normal' ) {
  *
  *
  *		upcastElementToAttribute( { view: 'strong', model: 'bold' } );
  *		upcastElementToAttribute( { view: 'strong', model: 'bold' } );
  *
  *
- *		upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'normal' );
+ *		upcastElementToAttribute( { view: 'strong', model: 'bold', priority: 'high' } );
  *
  *
  *		upcastElementToAttribute( {
  *		upcastElementToAttribute( {
  *			view: {
  *			view: {
@@ -136,10 +130,10 @@ export function upcastElementToElement( config, priority = 'normal' ) {
  * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  * If `String` is given, the model attribute value will be set to `true`.
  * If `String` is given, the model attribute value will be set to `true`.
- * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function upcastElementToAttribute( config, priority = 'low' ) {
+export function upcastElementToAttribute( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
 	_normalizeModelAttributeConfig( config );
 	_normalizeModelAttributeConfig( config );
@@ -150,7 +144,7 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
 	const eventName = elementName ? 'element:' + elementName : 'element';
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority } );
+		dispatcher.on( eventName, converter, { priority: config.priority || 'normal' } );
 	};
 	};
 }
 }
 
 
@@ -166,7 +160,7 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
  *
  *
  *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
  *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
  *
  *
- *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
+ *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', priority: 'normal' } );
  *
  *
  *		upcastAttributeToAttribute( {
  *		upcastAttributeToAttribute( {
  *			view: {
  *			view: {
@@ -178,7 +172,7 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
  *
  *
  *		upcastAttributeToAttribute( {
  *		upcastAttributeToAttribute( {
  *			view: {
  *			view: {
- *				name: 'span',
+ *				name: 'img',
  *				key: 'class',
  *				key: 'class',
  *				value: 'styled-dark'
  *				value: 'styled-dark'
  *			},
  *			},
@@ -215,10 +209,10 @@ export function upcastElementToAttribute( config, priority = 'low' ) {
  * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
  * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
  * If `String` is given, the model attribute value will be same as view attribute value.
  * If `String` is given, the model attribute value will be same as view attribute value.
- * @param {module:utils/priorities~PriorityString} [priority='low'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='low'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function upcastAttributeToAttribute( config, priority = 'low' ) {
+export function upcastAttributeToAttribute( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
 	let viewKey = null;
 	let viewKey = null;
@@ -232,7 +226,7 @@ export function upcastAttributeToAttribute( config, priority = 'low' ) {
 	const converter = _prepareToAttributeConverter( config, false );
 	const converter = _prepareToAttributeConverter( config, false );
 
 
 	return dispatcher => {
 	return dispatcher => {
-		dispatcher.on( 'element', converter, { priority } );
+		dispatcher.on( 'element', converter, { priority: config.priority || 'low' } );
 	};
 	};
 }
 }
 
 
@@ -246,7 +240,7 @@ export function upcastAttributeToAttribute( config, priority = 'low' ) {
  *
  *
  *		upcastElementToMarker( { view: 'marker-search', model: 'search' } );
  *		upcastElementToMarker( { view: 'marker-search', model: 'search' } );
  *
  *
- *		upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
+ *		upcastElementToMarker( { view: 'marker-search', model: 'search', priority: 'high' } );
  *
  *
  *		upcastElementToMarker( {
  *		upcastElementToMarker( {
  *			view: 'marker-search',
  *			view: 'marker-search',
@@ -269,15 +263,15 @@ export function upcastAttributeToAttribute( config, priority = 'low' ) {
  * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
  * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
  * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
  * a model marker name.
  * a model marker name.
- * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
  * @returns {Function} Conversion helper.
  */
  */
-export function upcastElementToMarker( config, priority = 'normal' ) {
+export function upcastElementToMarker( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
 
 
 	_normalizeToMarkerConfig( config );
 	_normalizeToMarkerConfig( config );
 
 
-	return upcastElementToElement( config, priority );
+	return upcastElementToElement( config );
 }
 }
 
 
 // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
 // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
@@ -313,6 +307,9 @@ function _prepareToElementConverter( config ) {
 			return;
 			return;
 		}
 		}
 
 
+		// Force consuming element's name.
+		match.match.name = true;
+
 		// Create model element basing on config.
 		// Create model element basing on config.
 		const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer );
 		const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer );
 
 
@@ -380,10 +377,8 @@ function _prepareToElementConverter( config ) {
 function _getModelElement( model, input, writer ) {
 function _getModelElement( model, input, writer ) {
 	if ( model instanceof Function ) {
 	if ( model instanceof Function ) {
 		return model( input, writer );
 		return model( input, writer );
-	} else if ( typeof model == 'string' ) {
-		return writer.createElement( model );
 	} else {
 	} else {
-		return model;
+		return writer.createElement( model );
 	}
 	}
 }
 }
 
 
@@ -399,13 +394,21 @@ function _normalizeViewAttributeKeyValueConfig( config ) {
 	}
 	}
 
 
 	const key = config.view.key;
 	const key = config.view.key;
-	const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+	let normalized;
 
 
-	const normalized = {
-		attribute: {
-			[ key ]: value
-		}
-	};
+	if ( key == 'class' || key == 'style' ) {
+		normalized = {
+			[ key ]: config.view.value
+		};
+	} else {
+		const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+
+		normalized = {
+			attribute: {
+				[ key ]: value
+			}
+		};
+	}
 
 
 	if ( config.view.name ) {
 	if ( config.view.name ) {
 		normalized.name = config.view.name;
 		normalized.name = config.view.name;
@@ -437,7 +440,8 @@ function _normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null )
 //
 //
 // @param {String} modelAttributeKey The key of the model attribute to set on a model node.
 // @param {String} modelAttributeKey The key of the model attribute to set on a model node.
 // @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
 // @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
-// @param {Boolean} consumeName If set to `true` converter will not consume element's name.
+// @param {Boolean} consumeName If set to `true` converter will try to consume name. If set to `false` converter will not try to
+// consume name. This flag overwrites parameter returned by `Matcher#match`.
 function _prepareToAttributeConverter( config, consumeName ) {
 function _prepareToAttributeConverter( config, consumeName ) {
 	const matcher = new Matcher( config.view );
 	const matcher = new Matcher( config.view );
 
 
@@ -460,6 +464,8 @@ function _prepareToAttributeConverter( config, consumeName ) {
 		if ( !consumeName ) {
 		if ( !consumeName ) {
 			// Do not test or consume `name` consumable.
 			// Do not test or consume `name` consumable.
 			delete match.match.name;
 			delete match.match.name;
+		} else {
+			match.match.name = true;
 		}
 		}
 
 
 		// Try to consume appropriate values from consumable values list.
 		// Try to consume appropriate values from consumable values list.
@@ -512,10 +518,10 @@ function _setAttributeOn( modelRange, modelAttribute, conversionApi ) {
 function _normalizeToMarkerConfig( config ) {
 function _normalizeToMarkerConfig( config ) {
 	const oldModel = config.model;
 	const oldModel = config.model;
 
 
-	config.model = ( viewElement, writer ) => {
+	config.model = ( viewElement, modelWriter ) => {
 		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
 		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
 
 
-		return writer.createElement( '$marker', { 'data-name': markerName } );
+		return modelWriter.createElement( '$marker', { 'data-name': markerName } );
 	};
 	};
 }
 }
 
 

+ 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.
 	 * Creates a `UpcastDispatcher` that operates using passed API.
 	 *
 	 *
 	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi
 	 * @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
 	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
 	 * by `UpcastDispatcher`.
 	 * 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
 		 * 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.
 		 * will be removed from conversion result.
@@ -153,62 +144,61 @@ export default class UpcastDispatcher {
 	 * @fires documentFragment
 	 * @fires documentFragment
 	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
 	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
 	 * Part of the view to be converted.
 	 * 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.
 	 * @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
 	 * @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
 	 * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
 	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
 	 * {@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 ) {
 	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 ) {
 	sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) {
@@ -658,7 +658,8 @@ class DebugPlugin extends Plugin {
 
 
 		const model = this.editor.model;
 		const model = this.editor.model;
 		const modelDocument = model.document;
 		const modelDocument = model.document;
-		const viewDocument = this.editor.editing.view;
+		const view = this.editor.editing.view;
+		const viewDocument = view.document;
 
 
 		modelDocument[ treeDump ] = [];
 		modelDocument[ treeDump ] = [];
 		viewDocument[ treeDump ] = [];
 		viewDocument[ treeDump ] = [];

+ 30 - 17
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -16,24 +16,25 @@ import Model from '../model/model';
 import Batch from '../model/batch';
 import Batch from '../model/batch';
 import ModelRange from '../model/range';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
 import ModelPosition from '../model/position';
-import DowncastDispatcher from '../conversion/downcastdispatcher';
 import ModelSelection from '../model/selection';
 import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
 import ModelDocumentFragment from '../model/documentfragment';
 import DocumentSelection from '../model/documentselection';
 import DocumentSelection from '../model/documentselection';
 
 
-import UpcastDispatcher from '../conversion/upcastdispatcher';
-import ViewDocument from '../view/document';
+import View from '../view/view';
 import ViewContainerElement from '../view/containerelement';
 import ViewContainerElement from '../view/containerelement';
-import ViewAttributeElement from '../view/attributeelement';
 import ViewRootEditableElement from '../view/rooteditableelement';
 import ViewRootEditableElement from '../view/rooteditableelement';
 
 
-import Mapper from '../conversion/mapper';
 import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
 import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
+
+import DowncastDispatcher from '../conversion/downcastdispatcher';
+import UpcastDispatcher from '../conversion/upcastdispatcher';
+import Mapper from '../conversion/mapper';
 import {
 import {
 	convertRangeSelection,
 	convertRangeSelection,
 	convertCollapsedSelection,
 	convertCollapsedSelection,
 } from '../conversion/downcast-selection-converters';
 } from '../conversion/downcast-selection-converters';
 import { insertText, insertElement, wrap } from '../conversion/downcast-converters';
 import { insertText, insertElement, wrap } from '../conversion/downcast-converters';
+
 import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
 import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
 
@@ -191,27 +192,35 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	}
 	}
 
 
 	// Set up conversion.
 	// 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' );
 	const viewRoot = new ViewRootEditableElement( 'div' );
 
 
 	// Create a temporary root element in view document.
 	// Create a temporary root element in view document.
-	viewRoot.document = viewDocument;
+	viewRoot._document = view.document;
 	viewRoot.rootName = 'main';
 	viewRoot.rootName = 'main';
 	viewDocument.roots.add( viewRoot );
 	viewDocument.roots.add( viewRoot );
 
 
 	// Create and setup downcast dispatcher.
 	// Create and setup downcast dispatcher.
-	const downcastDispatcher = new DowncastDispatcher( model, { mapper, viewSelection: viewDocument.selection } );
+	const downcastDispatcher = new DowncastDispatcher( { mapper } );
 
 
 	// Bind root elements.
 	// Bind root elements.
 	mapper.bindElements( node.root, viewRoot );
 	mapper.bindElements( node.root, viewRoot );
 
 
 	downcastDispatcher.on( 'insert:$text', insertText() );
 	downcastDispatcher.on( 'insert:$text', insertText() );
-	downcastDispatcher.on( 'attribute', wrap( ( value, data ) => {
+	downcastDispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
 		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
-			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
+			const converter = wrap( ( modelAttributeValue, viewWriter ) => {
+				return viewWriter.createAttributeElement(
+					'model-text-with-attributes',
+					{ [ data.attributeKey ]: stringifyAttributeValue( modelAttributeValue ) }
+				);
+			} );
+
+			converter( evt, data, conversionApi );
 		}
 		}
-	} ) );
+	} );
 	downcastDispatcher.on( 'insert', insertElement( modelItem => {
 	downcastDispatcher.on( 'insert', insertElement( modelItem => {
 		// Stringify object types values for properly display as an output string.
 		// Stringify object types values for properly display as an output string.
 		const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue );
 		const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue );
@@ -222,11 +231,12 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 
 
 	// Convert model to view.
 	// Convert model to view.
-	downcastDispatcher.convertInsert( range );
+	const writer = view._writer;
+	downcastDispatcher.convertInsert( range, writer );
 
 
 	// Convert model selection to view selection.
 	// Convert model selection to view selection.
 	if ( selection ) {
 	if ( selection ) {
-		downcastDispatcher.convertSelection( selection );
+		downcastDispatcher.convertSelection( selection, model.markers, writer );
 	}
 	}
 
 
 	// Parse view to data string.
 	// Parse view to data string.
@@ -235,7 +245,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	// Removing unneccessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
 	// Removing unneccessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
 	data = data.substr( 5, data.length - 11 );
 	data = data.substr( 5, data.length - 11 );
 
 
-	viewDocument.destroy();
+	view.destroy();
 
 
 	// Replace valid XML `model-text-with-attributes` element name to `$text`.
 	// Replace valid XML `model-text-with-attributes` element name to `$text`.
 	return data.replace( new RegExp( 'model-text-with-attributes', 'g' ), '$text' );
 	return data.replace( new RegExp( 'model-text-with-attributes', 'g' ), '$text' );
@@ -283,7 +293,8 @@ export function parse( data, schema, options = {} ) {
 	}
 	}
 
 
 	// Set up upcast dispatcher.
 	// 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( 'documentFragment', convertToModelFragment() );
 	upcastDispatcher.on( 'element:model-text-with-attributes', convertToModelText( true ) );
 	upcastDispatcher.on( 'element:model-text-with-attributes', convertToModelText( true ) );
@@ -293,7 +304,9 @@ export function parse( data, schema, options = {} ) {
 	upcastDispatcher.isDebug = true;
 	upcastDispatcher.isDebug = true;
 
 
 	// Convert view to model.
 	// 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 );
 	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.
  * 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 ViewDocumentFragment from '../view/documentfragment';
 import XmlDataProcessor from '../dataprocessor/xmldataprocessor';
 import XmlDataProcessor from '../dataprocessor/xmldataprocessor';
 import ViewElement from '../view/element';
 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.
  * 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 {Object} [options]
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will
  * not be included in the returned string.
  * not be included in the returned string.
@@ -49,11 +49,12 @@ const allowedTypes = {
  * (`<span view-priority="12">`, `<b view-priority="10">`).
  * (`<span view-priority="12">`, `<b view-priority="10">`).
  * @returns {String} The stringified data.
  * @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 withoutSelection = !!options.withoutSelection;
 	const rootName = options.rootName || 'main';
 	const rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
 	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.
  * 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 {String} data An HTML-like string to write into the document.
  * @param {Object} options
  * @param {Object} options
  * @param {String} [options.rootName='main'] The root name where parsed data will be stored. If not provided,
  * @param {String} [options.rootName='main'] The root name where parsed data will be stored. If not provided,
  * the default `main` name will be used.
  * 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 rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
 	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.
 // 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}.
  * {@link module:engine/view/attributeelement~AttributeElement attribute elements}.
  *
  *
  *		const attribute = new AttributeElement( 'b' );
  *		const attribute = new AttributeElement( 'b' );
- *		attribute.priority = 20;
+ *		attribute._priority = 20;
  *		getData( attribute, null, { showPriority: true } ); // <b view-priority="20"></b>
  *		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}
  * @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 ( newElement.is( 'attributeElement' ) ) {
 		if ( info.priority !== null ) {
 		if ( info.priority !== null ) {
-			newElement.priority = info.priority;
+			newElement._priority = info.priority;
 		}
 		}
 	}
 	}
 
 
 	// Move attributes.
 	// Move attributes.
 	for ( const attributeKey of viewElement.getAttributeKeys() ) {
 	for ( const attributeKey of viewElement.getAttributeKeys() ) {
-		newElement.setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
+		newElement._setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
 	}
 	}
 
 
 	return newElement;
 	return newElement;
@@ -947,7 +952,7 @@ function _convertElement( viewElement ) {
 function _convertElementNameAndPriority( viewElement ) {
 function _convertElementNameAndPriority( viewElement ) {
 	const parts = viewElement.name.split( ':' );
 	const parts = viewElement.name.split( ':' );
 	const priority = _convertPriority( viewElement.getAttribute( 'view-priority' ) );
 	const priority = _convertPriority( viewElement.getAttribute( 'view-priority' ) );
-	viewElement.removeAttribute( 'view-priority' );
+	viewElement._removeAttribute( 'view-priority' );
 
 
 	if ( parts.length == 1 ) {
 	if ( parts.length == 1 ) {
 		return {
 		return {

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

@@ -161,21 +161,16 @@ export default class Document {
 		// Buffer marker changes.
 		// Buffer marker changes.
 		// This is not covered in buffering operations because markers may change outside of them (when they
 		// This is not covered in buffering operations because markers may change outside of them (when they
 		// are modified using `model.markers` collection, not through `MarkerOperation`).
 		// are modified using `model.markers` collection, not through `MarkerOperation`).
-		this.listenTo( model.markers, 'set', ( evt, marker ) => {
-			// TODO: Should filter out changes of markers that are not in document.
-			// Whenever a new marker is added, buffer that change.
-			this.differ.bufferMarkerChange( marker.name, null, marker.getRange() );
-
-			// Whenever marker changes, buffer that.
-			marker.on( 'change', ( evt, oldRange ) => {
-				this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange() );
-			} );
-		} );
-
-		this.listenTo( model.markers, 'remove', ( evt, marker ) => {
-			// TODO: Should filter out changes of markers that are not in document.
-			// Whenever marker is removed, buffer that change.
-			this.differ.bufferMarkerChange( marker.name, marker.getRange(), null );
+		this.listenTo( model.markers, 'update', ( evt, marker, oldRange, newRange ) => {
+			// Whenever marker is updated, buffer that change.
+			this.differ.bufferMarkerChange( marker.name, oldRange, newRange );
+
+			if ( !oldRange ) {
+				// Whenever marker changes, buffer that.
+				marker.on( 'change', ( evt, oldRange ) => {
+					this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange() );
+				} );
+			}
 		} );
 		} );
 	}
 	}
 
 

+ 31 - 8
packages/ckeditor5-engine/src/model/documentselection.js

@@ -455,6 +455,16 @@ class LiveSelection extends Selection {
 		// @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
 		// @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
 		this._attributePriority = new Map();
 		this._attributePriority = new Map();
 
 
+		// Contains data required to fix ranges which have been moved to the graveyard.
+		// @private
+		// @member {Array} module:engine/model/liveselection~LiveSelection#_fixGraveyardRangesData
+		this._fixGraveyardRangesData = [];
+
+		// Flag that informs whether the selection ranges have changed. It is changed on true when `LiveRange#change:range` event is fired.
+		// @private
+		// @member {Array} module:engine/model/liveselection~LiveSelection#_hasChangedRange
+		this._hasChangedRange = false;
+
 		// Add events that will ensure selection correctness.
 		// Add events that will ensure selection correctness.
 		this.on( 'change:range', () => {
 		this.on( 'change:range', () => {
 			for ( const range of this.getRanges() ) {
 			for ( const range of this.getRanges() ) {
@@ -497,6 +507,20 @@ class LiveSelection extends Selection {
 				clearAttributesStoredInElement( operation, this._model, batch );
 				clearAttributesStoredInElement( operation, this._model, batch );
 			}
 			}
 		}, { priority: 'low' } );
 		}, { priority: 'low' } );
+
+		this.listenTo( this._model, 'applyOperation', () => {
+			while ( this._fixGraveyardRangesData.length ) {
+				const { liveRange, sourcePosition } = this._fixGraveyardRangesData.shift();
+
+				this._fixGraveyardSelection( liveRange, sourcePosition );
+			}
+
+			if ( this._hasChangedRange ) {
+				this._hasChangedRange = false;
+
+				this.fire( 'change:range', { directChange: false } );
+			}
+		}, { priority: 'lowest' } );
 	}
 	}
 
 
 	get isCollapsed() {
 	get isCollapsed() {
@@ -618,13 +642,15 @@ class LiveSelection extends Selection {
 		const liveRange = LiveRange.createFromRange( range );
 		const liveRange = LiveRange.createFromRange( range );
 
 
 		liveRange.on( 'change:range', ( evt, oldRange, data ) => {
 		liveRange.on( 'change:range', ( evt, oldRange, data ) => {
-			// If `LiveRange` is in whole moved to the graveyard, fix that range.
+			this._hasChangedRange = true;
+
+			// If `LiveRange` is in whole moved to the graveyard, save necessary data. It will be fixed on `Model#applyOperation` event.
 			if ( liveRange.root == this._document.graveyard ) {
 			if ( liveRange.root == this._document.graveyard ) {
-				this._fixGraveyardSelection( liveRange, data.sourcePosition );
+				this._fixGraveyardRangesData.push( {
+					liveRange,
+					sourcePosition: data.sourcePosition
+				} );
 			}
 			}
-
-			// Whenever a live range from selection changes, fire an event informing about that change.
-			this.fire( 'change:range', { directChange: false } );
 		} );
 		} );
 
 
 		return liveRange;
 		return liveRange;
@@ -890,9 +916,6 @@ class LiveSelection extends Selection {
 			this._ranges.splice( index, 0, newRange );
 			this._ranges.splice( index, 0, newRange );
 		}
 		}
 		// If nearest valid selection range cannot be found - just removing the old range is fine.
 		// If nearest valid selection range cannot be found - just removing the old range is fine.
-
-		// Fire an event informing about selection change.
-		this.fire( 'change:range', { directChange: false } );
 	}
 	}
 }
 }
 
 

+ 92 - 41
packages/ckeditor5-engine/src/model/markercollection.js

@@ -17,8 +17,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 /**
 /**
  * The collection of all {@link module:engine/model/markercollection~Marker markers} attached to the document.
  * The collection of all {@link module:engine/model/markercollection~Marker markers} attached to the document.
  * It lets you {@link module:engine/model/markercollection~MarkerCollection#get get} markers or track them using
  * It lets you {@link module:engine/model/markercollection~MarkerCollection#get get} markers or track them using
- * {@link module:engine/model/markercollection~MarkerCollection#event:set} and
- * {@link module:engine/model/markercollection~MarkerCollection#event:remove} events.
+ * {@link module:engine/model/markercollection~MarkerCollection#event:update} event.
  *
  *
  * To create, change or remove makers use {@link module:engine/model/writer~Writer model writers'} methods:
  * To create, change or remove makers use {@link module:engine/model/writer~Writer model writers'} methods:
  * {@link module:engine/model/writer~Writer#setMarker} or {@link module:engine/model/writer~Writer#removeMarker}. Since
  * {@link module:engine/model/writer~Writer#setMarker} or {@link module:engine/model/writer~Writer#removeMarker}. Since
@@ -79,37 +78,48 @@ export default class MarkerCollection {
 	 * Creates and adds a {@link ~Marker marker} to the `MarkerCollection` with given name on given
 	 * Creates and adds a {@link ~Marker marker} to the `MarkerCollection` with given name on given
 	 * {@link module:engine/model/range~Range range}.
 	 * {@link module:engine/model/range~Range range}.
 	 *
 	 *
-	 * If `MarkerCollection` already had a marker with given name (or {@link ~Marker marker} was passed) and the range to
-	 * set is different, the marker in collection is removed and then new marker is added. If the range was same, nothing
-	 * happens and `false` is returned.
+	 * If `MarkerCollection` already had a marker with given name (or {@link ~Marker marker} was passed), the marker in
+	 * collection is updated and {@link module:engine/model/markercollection~MarkerCollection#event:update} event is fired
+	 * but only if there was a change (marker range or {@link module:engine/model/markercollection~Marker#managedUsingOperations}
+	 * flag has changed.
 	 *
 	 *
 	 * @protected
 	 * @protected
-	 * @fires module:engine/model/markercollection~MarkerCollection#event:set
-	 * @fires module:engine/model/markercollection~MarkerCollection#event:remove
+	 * @fires module:engine/model/markercollection~MarkerCollection#event:update
 	 * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to set or marker instance to update.
 	 * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to set or marker instance to update.
 	 * @param {module:engine/model/range~Range} range Marker range.
 	 * @param {module:engine/model/range~Range} range Marker range.
-	 * @param {Boolean} managedUsingOperations Specifies whether the marker is managed using operations.
-	 * @returns {module:engine/model/markercollection~Marker} `Marker` instance added to the collection.
+	 * @param {Boolean} [managedUsingOperations=false] Specifies whether the marker is managed using operations.
+	 * @returns {module:engine/model/markercollection~Marker} `Marker` instance which was added or updated.
 	 */
 	 */
-	_set( markerOrName, range, managedUsingOperations ) {
+	_set( markerOrName, range, managedUsingOperations = false ) {
 		const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
 		const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
 		const oldMarker = this._markers.get( markerName );
 		const oldMarker = this._markers.get( markerName );
 
 
 		if ( oldMarker ) {
 		if ( oldMarker ) {
 			const oldRange = oldMarker.getRange();
 			const oldRange = oldMarker.getRange();
+			let hasChanged = false;
+
+			if ( !oldRange.isEqual( range ) ) {
+				oldMarker._attachLiveRange( LiveRange.createFromRange( range ) );
+				hasChanged = true;
+			}
 
 
-			if ( oldRange.isEqual( range ) && managedUsingOperations === oldMarker.managedUsingOperations ) {
-				return oldMarker;
+			if ( managedUsingOperations != oldMarker.managedUsingOperations ) {
+				oldMarker._managedUsingOperations = managedUsingOperations;
+				hasChanged = true;
 			}
 			}
 
 
-			this._remove( markerName );
+			if ( hasChanged ) {
+				this.fire( 'update:' + markerName, oldMarker, oldRange, range );
+			}
+
+			return oldMarker;
 		}
 		}
 
 
 		const liveRange = LiveRange.createFromRange( range );
 		const liveRange = LiveRange.createFromRange( range );
 		const marker = new Marker( markerName, liveRange, managedUsingOperations );
 		const marker = new Marker( markerName, liveRange, managedUsingOperations );
 
 
 		this._markers.set( markerName, marker );
 		this._markers.set( markerName, marker );
-		this.fire( 'set:' + markerName, marker );
+		this.fire( 'update:' + markerName, marker, null, range );
 
 
 		return marker;
 		return marker;
 	}
 	}
@@ -118,6 +128,7 @@ export default class MarkerCollection {
 	 * Removes given {@link ~Marker marker} or a marker with given name from the `MarkerCollection`.
 	 * Removes given {@link ~Marker marker} or a marker with given name from the `MarkerCollection`.
 	 *
 	 *
 	 * @protected
 	 * @protected
+	 * @fires module:engine/model/markercollection~MarkerCollection#event:update
 	 * @param {String} markerOrName Marker or name of a marker to remove.
 	 * @param {String} markerOrName Marker or name of a marker to remove.
 	 * @returns {Boolean} `true` if marker was found and removed, `false` otherwise.
 	 * @returns {Boolean} `true` if marker was found and removed, `false` otherwise.
 	 */
 	 */
@@ -127,7 +138,7 @@ export default class MarkerCollection {
 
 
 		if ( oldMarker ) {
 		if ( oldMarker ) {
 			this._markers.delete( markerName );
 			this._markers.delete( markerName );
-			this.fire( 'remove:' + markerName, oldMarker );
+			this.fire( 'update:' + markerName, oldMarker, oldMarker.getRange(), null );
 
 
 			this._destroyMarker( oldMarker );
 			this._destroyMarker( oldMarker );
 
 
@@ -193,22 +204,18 @@ export default class MarkerCollection {
 	 */
 	 */
 	_destroyMarker( marker ) {
 	_destroyMarker( marker ) {
 		marker.stopListening();
 		marker.stopListening();
-		marker._liveRange.detach();
-		marker._liveRange = null;
+		marker._detachLiveRange();
 	}
 	}
 
 
 	/**
 	/**
-	 * Fired whenever marker is added or updated in `MarkerCollection`.
+	 * Fired whenever marker is added, updated or removed from `MarkerCollection`.
 	 *
 	 *
-	 * @event set
-	 * @param {module:engine/model/markercollection~Marker} The set marker.
-	 */
-
-	/**
-	 * Fired whenever marker is removed from `MarkerCollection`.
-	 *
-	 * @event remove
-	 * @param {module:engine/model/markercollection~Marker} marker The removed marker.
+	 * @event update
+	 * @param {module:engine/model/markercollection~Marker} Updated Marker.
+	 * @param {module:engine/model/range~Range|null} oldRange Marker range before the update. When is not defined it
+	 * means that marker is just added.
+	 * @param {module:engine/model/range~Range|null} newRange Marker range after update. When is not defined it
+	 * means that marker is just removed.
 	 */
 	 */
 }
 }
 
 
@@ -240,7 +247,7 @@ mix( MarkerCollection, EmitterMixin );
  * Name is used to group and identify markers. Names have to be unique, but markers can be grouped by
  * Name is used to group and identify markers. Names have to be unique, but markers can be grouped by
  * using common prefixes, separated with `:`, for example: `user:john` or `search:3`. That's useful in term of creating
  * using common prefixes, separated with `:`, for example: `user:john` or `search:3`. That's useful in term of creating
  * namespaces for custom elements (e.g. comments, highlights). You can use this prefixes in
  * namespaces for custom elements (e.g. comments, highlights). You can use this prefixes in
- * {@link module:engine/model/markercollection~MarkerCollection#event:set} listeners to listen on changes in a group of markers.
+ * {@link module:engine/model/markercollection~MarkerCollection#event:update} listeners to listen on changes in a group of markers.
  * For instance: `model.markers.on( 'set:user', callback );` will be called whenever any `user:*` markers changes.
  * For instance: `model.markers.on( 'set:user', callback );` will be called whenever any `user:*` markers changes.
  *
  *
  * There are two types of markers.
  * There are two types of markers.
@@ -291,6 +298,7 @@ class Marker {
 	 *
 	 *
 	 * @param {String} name Marker name.
 	 * @param {String} name Marker name.
 	 * @param {module:engine/model/liverange~LiveRange} liveRange Range marked by the marker.
 	 * @param {module:engine/model/liverange~LiveRange} liveRange Range marked by the marker.
+	 * @param {Boolean} managedUsingOperations Specifies whether the marker is managed using operations.
 	 */
 	 */
 	constructor( name, liveRange, managedUsingOperations ) {
 	constructor( name, liveRange, managedUsingOperations ) {
 		/**
 		/**
@@ -302,25 +310,35 @@ class Marker {
 		this.name = name;
 		this.name = name;
 
 
 		/**
 		/**
-		 * Flag indicates if the marker is managed using operations or not. See {@link ~Marker marker class description}
-		 * to learn more about marker types. See {@link module:engine/model/writer~Writer#setMarker}.
+		 * Flag indicates if the marker is managed using operations or not.
 		 *
 		 *
-		 * @readonly
-		 * @type {Boolean}
+		 * @protected
+		 * @member {Boolean}
 		 */
 		 */
-		this.managedUsingOperations = managedUsingOperations;
+		this._managedUsingOperations = managedUsingOperations;
 
 
 		/**
 		/**
 		 * Range marked by the marker.
 		 * Range marked by the marker.
 		 *
 		 *
-		 * @protected
-		 * @type {module:engine/model/liverange~LiveRange}
+		 * @private
+		 * @member {module:engine/model/liverange~LiveRange} #_liveRange
 		 */
 		 */
-		this._liveRange = liveRange;
+		this._liveRange = this._attachLiveRange( liveRange );
+	}
 
 
-		// Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
-		this._liveRange.delegate( 'change:range' ).to( this );
-		this._liveRange.delegate( 'change:content' ).to( this );
+	/**
+	 * Returns value of flag indicates if the marker is managed using operations or not.
+	 * See {@link ~Marker marker class description} to learn more about marker types.
+	 * See {@link module:engine/model/writer~Writer#setMarker}.
+	 *
+	 * @returns {Boolean}
+	 */
+	get managedUsingOperations() {
+		if ( !this._liveRange ) {
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+		}
+
+		return this._managedUsingOperations;
 	}
 	}
 
 
 	/**
 	/**
@@ -369,13 +387,46 @@ class Marker {
 		return Range.createFromRange( this._liveRange );
 		return Range.createFromRange( this._liveRange );
 	}
 	}
 
 
+	/**
+	 * Binds new live range to marker and detach the old one if is attached.
+	 *
+	 * @protected
+	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to attach
+	 * @return {module:engine/model/liverange~LiveRange} Attached live range.
+	 */
+	_attachLiveRange( liveRange ) {
+		if ( this._liveRange ) {
+			this._detachLiveRange();
+		}
+
+		// Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
+		liveRange.delegate( 'change:range' ).to( this );
+		liveRange.delegate( 'change:content' ).to( this );
+
+		this._liveRange = liveRange;
+
+		return liveRange;
+	}
+
+	/**
+	 * Unbinds and destroys currently attached live range.
+	 *
+	 * @protected
+	 */
+	_detachLiveRange() {
+		this._liveRange.stopDelegating( 'change:range', this );
+		this._liveRange.stopDelegating( 'change:content', this );
+		this._liveRange.detach();
+		this._liveRange = null;
+	}
+
 	/**
 	/**
 	 * Fired whenever {@link ~Marker#_liveRange marker range} is changed due to changes on {@link module:engine/model/document~Document}.
 	 * Fired whenever {@link ~Marker#_liveRange marker range} is changed due to changes on {@link module:engine/model/document~Document}.
 	 * This is a delegated {@link module:engine/model/liverange~LiveRange#event:change:range LiveRange change:range event}.
 	 * This is a delegated {@link module:engine/model/liverange~LiveRange#event:change:range LiveRange change:range event}.
 	 *
 	 *
 	 * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
 	 * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
 	 * all event listeners listening to it should be removed. It is best to do it on
 	 * all event listeners listening to it should be removed. It is best to do it on
-	 * {@link module:engine/model/markercollection~MarkerCollection#event:remove MarkerCollection remove event}.
+	 * {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
 	 *
 	 *
 	 * @see module:engine/model/liverange~LiveRange#event:change:range
 	 * @see module:engine/model/liverange~LiveRange#event:change:range
 	 * @event change:range
 	 * @event change:range
@@ -389,7 +440,7 @@ class Marker {
 	 *
 	 *
 	 * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
 	 * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
 	 * all event listeners listening to it should be removed. It is best to do it on
 	 * all event listeners listening to it should be removed. It is best to do it on
-	 * {@link module:engine/model/markercollection~MarkerCollection#event:remove MarkerCollection remove event}.
+	 * {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
 	 *
 	 *
 	 * @see module:engine/model/liverange~LiveRange#event:change:content
 	 * @see module:engine/model/liverange~LiveRange#event:change:content
 	 * @event change:content
 	 * @event change:content

+ 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 { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
 import DocumentSelection from '../documentselection';
 import DocumentSelection from '../documentselection';
 
 
+const wordBoundaryCharacters = ' ,.?!:;"-()';
+
 /**
 /**
  * Modifies the selection. Currently, the supported modifications are:
  * 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
  *  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
  *  outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
  *  extension will include whole "surrogate pair".
  *  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.
  * **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 {module:engine/model/selection~Selection} selection The selection to modify.
  * @param {Object} [options]
  * @param {Object} [options]
  * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
  * @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 = {} ) {
 export default function modifySelection( model, selection, options = {} ) {
 	const schema = model.schema;
 	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).
 // 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 ) {
 function tryExtendingTo( data, value ) {
 	// If found text, we can certainly put the focus in it. Let's just find a correct position
 	// If found text, we can certainly put the focus in it. Let's just find a correct position
 	// based on the unit.
 	// based on the unit.
 	if ( value.type == 'text' ) {
 	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.
 	// 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
 // 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.
 // 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 ) {
 function getCorrectPosition( walker, unit ) {
 	const textNode = walker.position.textNode;
 	const textNode = walker.position.textNode;
 
 
@@ -134,6 +146,45 @@ function getCorrectPosition( walker, unit ) {
 	return walker.position;
 	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 ) {
 function getSearchRange( start, isForward ) {
 	const root = start.root;
 	const root = start.root;
 	const searchEnd = Position.createAt( root, isForward ? 'end' : 0 );
 	const searchEnd = Position.createAt( root, isForward ? 'end' : 0 );
@@ -144,3 +195,24 @@ function getSearchRange( start, isForward ) {
 		return new Range( searchEnd, start );
 		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 - 16
packages/ckeditor5-engine/src/model/writer.js

@@ -761,34 +761,32 @@ export default class Writer {
 	}
 	}
 
 
 	/**
 	/**
-	 * Adds or updates {@link module:engine/model/markercollection~Marker marker}.
+	 * Adds or updates a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
+	 * changes in the document and updates its range automatically, when model tree changes. Still, it is possible to change the
+	 * marker's range directly using this method.
 	 *
 	 *
 	 * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
 	 * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
 	 * name is created and returned.
 	 * name is created and returned.
 	 *
 	 *
-	 * Using this method you can change markers range or define if the marker is managed by operation or not.
+	 * The `options.usingOperation` parameter lets you decide if the marker should be managed by operations or not. See
+	 * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
+	 * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
+	 * This is useful when a marker have been created earlier and then later, it needs to be added to the document history.
 	 *
 	 *
-	 * Marker tracks changes is the document and updates the range automatically, so you need to update the range only
-	 * when it changes directly. You do not need to update it after each document change.
+	 * Create/update marker directly base on marker's name:
 	 *
 	 *
-	 * The option parameter let you decide if the marker should be managed by operations or not. See
-	 * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
-	 * markers managed by operation and managed directly. You can change this option for existing marker. This is
-	 * useful if a marker have been created earlier and need to be added to the document history later.
+	 * 		setMarker( markerName, range );
 	 *
 	 *
 	 * Update marker using operation:
 	 * Update marker using operation:
 	 *
 	 *
 	 * 		setMarker( marker, range, { usingOperation: true } );
 	 * 		setMarker( marker, range, { usingOperation: true } );
-	 *
-	 * Create/update marker directly base on marker's name:
-	 *
-	 * 		setMarker( markerName, range );
+	 * 		setMarker( markerName, range, { usingOperation: true } );
 	 *
 	 *
 	 * Create marker with a unique id using operation:
 	 * Create marker with a unique id using operation:
 	 *
 	 *
 	 * 		setMarker( range, { usingOperation: true } );
 	 * 		setMarker( range, { usingOperation: true } );
 	 *
 	 *
-	 * Create marker directly with a unique name:
+	 * Create marker directly without using operations:
 	 *
 	 *
 	 * 		setMarker( range )
 	 * 		setMarker( range )
 	 *
 	 *
@@ -799,9 +797,9 @@ export default class Writer {
 	 * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
 	 * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
 	 *
 	 *
 	 * @see module:engine/model/markercollection~Marker
 	 * @see module:engine/model/markercollection~Marker
-	 * @param {module:engine/model/markercollection~Marker|String} [markerOrName=uid()]
-	 * Name of marker to add, Marker instance to update or range for the marker with a unique name.
-	 * @param {module:engine/model/range~Range|Object} [range] Marker range or options.
+	 * @param {module:engine/model/markercollection~Marker|String} [markerOrName]
+	 * Name of a marker to create or update, or `Marker` instance to update, or range for the marker with a unique name.
+	 * @param {module:engine/model/range~Range} [range] Marker range.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.usingOperation=false] Flag indicated whether the marker should be added by MarkerOperation.
 	 * @param {Boolean} [options.usingOperation=false] Flag indicated whether the marker should be added by MarkerOperation.
 	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
 	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.

+ 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 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
  * 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}
  * 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
 		 * {@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.
  		 * nodes may prevent merging, e.g. two `<abbr>` nodes next each other shouldn't be merged.
 		 *
 		 *
+		 * @protected
 		 * @member {Number}
 		 * @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.
 		 * 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;
 		this.getFillerOffset = getFillerOffset;
 	}
 	}
 
 
+	/**
+	 * Priority of this element.
+	 *
+	 * @readonly
+	 * @return {Number}
+	 */
+	get priority() {
+		return this._priority;
+	}
+
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
@@ -71,7 +82,7 @@ export default class AttributeElement extends Element {
 		const cloned = super.clone( deep );
 		const cloned = super.clone( deep );
 
 
 		// Clone priority too.
 		// Clone priority too.
-		cloned.priority = this.priority;
+		cloned._priority = this._priority;
 
 
 		return cloned;
 		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
  * 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.
  * 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}
  * merging}
  * is performed only in a bounds of a container nodes.
  * is performed only in a bounds of a container nodes.
  *
  *
@@ -32,7 +32,7 @@ import Element from './element';
  *
  *
  *		<p><b>fo^o</b></p>
  *		<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>
  *		<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 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 Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 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
  * @mixes module:utils/observablemixin~ObservableMixin
  */
  */
@@ -47,14 +23,6 @@ export default class Document {
 	 * Creates a Document instance.
 	 * Creates a Document instance.
 	 */
 	 */
 	constructor() {
 	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.
 		 * Selection done on this document.
 		 *
 		 *
@@ -63,16 +31,6 @@ export default class Document {
 		 */
 		 */
 		this.selection = new Selection();
 		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}.
 		 * 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
 		 * @member {Boolean} module:engine/view/document~Document#isFocused
 		 */
 		 */
 		this.set( 'isFocused', false );
 		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' ) {
 	getRoot( name = 'main' ) {
 		return this.roots.get( name );
 		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 );
 mix( Document, ObservableMixin );
@@ -326,10 +93,3 @@ mix( Document, ObservableMixin );
  *
  *
  * @typedef {String} module:engine/view/document~ChangeType
  * @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;
 				const attrs = domNode.attributes;
 
 
 				for ( let i = attrs.length - 1; i >= 0; i-- ) {
 				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.
 		 * 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
 		 * @readonly
 		 * @observable
 		 * @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() {
 	get document() {
 		return this.getCustomProperty( documentSymbol );
 		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 ) ) {
 		if ( this.getCustomProperty( documentSymbol ) ) {
 			/**
 			/**
 			 * View document is already set. It can only be set once.
 			 * 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.' );
 			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 );
 		this.bind( 'isReadOnly' ).to( document );
 
 
@@ -84,11 +96,10 @@ export default class EditableElement extends ContainerElement {
 			isFocused => isFocused && document.selection.editableElement == this
 			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;
 			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 );
 		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
 	 * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
 	 * this element.
 	 * this element.
@@ -369,42 +348,6 @@ export default class Element extends Node {
 		return count;
 		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`.
 	 * 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;
 		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.
 	 * Returns true if class is present.
 	 * If more then one class is provided - returns true only when all classes are 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();
 		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.
 	 * Returns style value for given property.
 	 * Undefined is returned if style does not exist.
 	 * Undefined is returned if style does not exist.
@@ -598,20 +486,6 @@ export default class Element extends Node {
 		return true;
 		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.
 	 * 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.
 	 * 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;
 		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.
 	 * Returns the custom property value for the given key.
 	 *
 	 *
@@ -657,16 +520,6 @@ export default class Element extends Node {
 		return this._customProperties.get( key );
 		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.
 	 * Returns an iterator which iterates over this element's custom properties.
 	 * Iterator provides `[ key, value ]` pairs for each stored property.
 	 * Iterator provides `[ key, value ]` pairs for each stored property.
@@ -686,12 +539,12 @@ export default class Element extends Node {
  	 *
  	 *
 	 * For example:
 	 * 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"'
 	 *		// returns 'foo class="baz" style="border-color:white;color:red" apple="20" banana="10"'
 	 *		element.getIdentity();
 	 *		element.getIdentity();
@@ -711,6 +564,166 @@ export default class Element extends Node {
 			( attributes == '' ? '' : ` ${ attributes }` );
 			( 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.
 	 * 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
  * 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.
  * 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.
 // 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.
  * {@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
  * 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
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
  */
 export default class ClickObserver extends DomEventObserver {
 export default class ClickObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		this.domEventType = 'click';
 		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
  * Note that this event is not available by default. To make it available
  * {@link module:engine/view/observer/clickobserver~ClickObserver} needs to be added
  * {@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
  * @see module:engine/view/observer/clickobserver~ClickObserver
  * @event module:engine/view/document~Document#event:click
  * @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 {
 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 {Event} domEvent The DOM event.
 	 * @param {Object} [additionalData] Additional properties that the instance should contain.
 	 * @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.
 		 * The instance of the document.
 		 *
 		 *
 		 * @readonly
 		 * @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.
 		 * The DOM event.
@@ -55,7 +63,7 @@ export default class DomEventData {
 	 * @type module:engine/view/element~Element
 	 * @type module:engine/view/element~Element
 	 */
 	 */
 	get target() {
 	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
 	 * @inheritDoc
 	 */
 	 */
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		/**
 		/**
 		 * If set to `true` DOM events will be listened on the capturing phase.
 		 * 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 ) {
 	fire( eventType, domEvent, additionalData ) {
 		if ( this.isEnabled ) {
 		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.
 	 * 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.
 		 * 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 ) {
 	_handleSelectionMove( keyCode ) {
 		const selection = this.document.selection;
 		const selection = this.document.selection;
 		const newSelection = new ViewSelection( selection );
 		const newSelection = new ViewSelection( selection );
-		newSelection.setFake( false );
+		newSelection._setFake( false );
 
 
 		// Left or up arrow pressed - move selection to start.
 		// Left or up arrow pressed - move selection to start.
 		if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) {
 		if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) {
-			newSelection.setTo( newSelection.getFirstPosition() );
+			newSelection._setTo( newSelection.getFirstPosition() );
 		}
 		}
 
 
 		// Right or down arrow pressed - move selection to end.
 		// Right or down arrow pressed - move selection to end.
 		if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) {
 		if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) {
-			newSelection.setTo( newSelection.getLastPosition() );
+			newSelection._setTo( newSelection.getLastPosition() );
 		}
 		}
 
 
 		const data = {
 		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
  * Focus observer handle also {@link module:engine/view/rooteditableelement~RootEditableElement#isFocused isFocused} property of the
  * {@link module:engine/view/rooteditableelement~RootEditableElement root elements}.
  * {@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
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
  */
 export default class FocusObserver extends DomEventObserver {
 export default class FocusObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		this.domEventType = [ 'focus', 'blur' ];
 		this.domEventType = [ 'focus', 'blur' ];
 		this.useCapture = true;
 		this.useCapture = true;
+		const document = this.document;
 
 
 		document.on( 'focus', () => {
 		document.on( 'focus', () => {
 			document.isFocused = true;
 			document.isFocused = true;
@@ -36,7 +37,7 @@ export default class FocusObserver extends DomEventObserver {
 			// overwrite new DOM selection with selection from the view.
 			// overwrite new DOM selection with selection from the view.
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
 			// 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.
 			// 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 ) => {
 		document.on( 'blur', ( evt, data ) => {
@@ -46,7 +47,7 @@ export default class FocusObserver extends DomEventObserver {
 				document.isFocused = false;
 				document.isFocused = false;
 
 
 				// Re-render the document to update view elements.
 				// 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}.
  * Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
  *
  *
  * Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
  * 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
  * @see module:engine/view/observer/focusobserver~FocusObserver
  * @event module:engine/view/document~Document#event:focus
  * @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}.
  * Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
  *
  *
  * Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
  * 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
  * @see module:engine/view/observer/focusobserver~FocusObserver
  * @event module:engine/view/document~Document#event:blur
  * @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.
  * {@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
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
  */
 export default class KeyObserver extends DomEventObserver {
 export default class KeyObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		this.domEventType = [ 'keydown', 'keyup' ];
 		this.domEventType = [ 'keydown', 'keyup' ];
 	}
 	}
@@ -45,8 +45,7 @@ export default class KeyObserver extends DomEventObserver {
  * Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
  * Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
  *
  *
  * Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
  * 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
  * @see module:engine/view/observer/keyobserver~KeyObserver
  * @event module:engine/view/document~Document#event:keydown
  * @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}.
  * Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
  *
  *
  * Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
  * 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
  * @see module:engine/view/observer/keyobserver~KeyObserver
  * @event module:engine/view/document~Document#event:keyup
  * @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.
  * Mouse events observer.
  *
  *
  * Note that this observer is not available by default. To make it available it needs to be added to
  * 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
  * @extends module:engine/view/observer/domeventobserver~DomEventObserver
  */
  */
 export default class MouseObserver extends DomEventObserver {
 export default class MouseObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		this.domEventType = 'mousedown';
 		this.domEventType = 'mousedown';
 	}
 	}
@@ -36,7 +35,7 @@ export default class MouseObserver extends DomEventObserver {
  * Introduced by {@link module:engine/view/observer/mouseobserver~MouseObserver}.
  * 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}
  * 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
  * @see module:engine/view/observer/mouseobserver~MouseObserver
  * @event module:engine/view/document~Document#event:mousedown
  * @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
  * 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.
  * {@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
  * @extends module:engine/view/observer/observer~Observer
  */
  */
 export default class MutationObserver extends Observer {
 export default class MutationObserver extends Observer {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		/**
 		/**
 		 * Native mutation observer config.
 		 * 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}
 		 * @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}
 		 * @member {module:engine/view/renderer~Renderer}
 		 */
 		 */
-		this.renderer = document.renderer;
+		this.renderer = view._renderer;
 
 
 		/**
 		/**
 		 * Observed DOM elements.
 		 * Observed DOM elements.
@@ -240,8 +240,8 @@ export default class MutationObserver extends Observer {
 			// Anchor and focus has to be properly mapped to view.
 			// Anchor and focus has to be properly mapped to view.
 			if ( viewSelectionAnchor && viewSelectionFocus ) {
 			if ( viewSelectionAnchor && viewSelectionFocus ) {
 				viewSelection = new ViewSelection();
 				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
 		// 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.
 		// 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 ) {
 		function sameNodes( child1, child2 ) {
 			// First level of comparison (array of children vs array of children) – use the Lodash's default behavior.
 			// 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.
  * mutation, so all changes which should be applied, should be handled on this event.
  *
  *
  * Introduced by {@link module:engine/view/observer/mutationobserver~MutationObserver}.
  * Introduced by {@link module:engine/view/observer/mutationobserver~MutationObserver}.
  *
  *
  * Note that because {@link module:engine/view/observer/mutationobserver~MutationObserver} is attached by the
  * 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
  * @see module:engine/view/observer/mutationobserver~MutationObserver
  * @event module:engine/view/document~Document#event:mutations
  * @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.
 	 * 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.
 		 * Reference to the {@link module:engine/view/document~Document} object.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
 		 * @member {module:engine/view/document~Document}
 		 * @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.
 		 * 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).
 	 * (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.
 	 * 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
 	 * 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
 	 * @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
  * {@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.
  * 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
  * @see module:engine/view/observer/mutationobserver~MutationObserver
  * @extends module:engine/view/observer/observer~Observer
  * @extends module:engine/view/observer/observer~Observer
  */
  */
 export default class SelectionObserver extends Observer {
 export default class SelectionObserver extends Observer {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		/**
 		/**
 		 * Instance of the mutation observer. Selection observer calls
 		 * 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}
 		 * @member {module:engine/view/observer/mutationobserver~MutationObserver}
 		 * module:engine/view/observer/selectionobserver~SelectionObserver#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.
 		 * 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
 		 * @readonly
 		 * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#selection
 		 * @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 */
 		/* 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
 		 * @readonly
 		 * @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
 		 * @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
 		 */
 		 */
 		/* eslint-enable max-len */
 		/* 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
 		 * 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 ( this.selection.isSimilar( newViewSelection ) ) {
 			// If selection was equal and we are at this point of algorithm, it means that it was incorrect.
 			// 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.
 			// Just re-render it, no need to fire any events, etc.
-			this.document.render();
+			this.view.render();
 		} else {
 		} else {
 			const data = {
 			const data = {
 				oldSelection: this.selection,
 				oldSelection: this.selection,
@@ -208,8 +200,7 @@ export default class SelectionObserver extends Observer {
  * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  *
  *
  * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
  * 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
  * @see module:engine/view/observer/selectionobserver~SelectionObserver
  * @event module:engine/view/document~Document#event:selectionChange
  * @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}.
  * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
  *
  *
  * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
  * 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
  * @see module:engine/view/observer/selectionobserver~SelectionObserver
  * @event module:engine/view/document~Document#event:selectionChangeDone
  * @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 extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import '../../theme/placeholder.css';
 import '../../theme/placeholder.css';
 
 
 const listener = {};
 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.
  * @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.
  * 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.
 	// Detach placeholder if was used before.
-	detachPlaceholder( element );
+	detachPlaceholder( view, element );
 
 
 	// Single listener per document.
 	// Single listener per document.
 	if ( !documentPlaceholders.has( document ) ) {
 	if ( !documentPlaceholders.has( document ) ) {
 		documentPlaceholders.set( document, new Map() );
 		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.
 	// Store text in element's data attribute.
 	// This data attribute is used in CSS class to show the placeholder.
 	// 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.
 	// Store information about placeholder.
 	documentPlaceholders.get( document ).set( element, checkFunction );
 	documentPlaceholders.get( document ).set( element, checkFunction );
 
 
 	// Update right away too.
 	// Update right away too.
-	updateSinglePlaceholder( element, checkFunction );
+	updateSinglePlaceholder( view, element, checkFunction );
 }
 }
 
 
 /**
 /**
  * Removes placeholder functionality from given element.
  * Removes placeholder functionality from given element.
  *
  *
+ * @param {module:engine/view/view~View} view
  * @param {module:engine/view/element~Element} element
  * @param {module:engine/view/element~Element} element
  */
  */
-export function detachPlaceholder( element ) {
+export function detachPlaceholder( view, element ) {
 	const document = element.document;
 	const document = element.document;
 
 
-	element.removeClass( 'ck-placeholder' );
-	element.removeAttribute( 'data-placeholder' );
-
 	if ( documentPlaceholders.has( document ) ) {
 	if ( documentPlaceholders.has( document ) ) {
 		documentPlaceholders.get( document ).delete( element );
 		documentPlaceholders.get( document ).delete( element );
 	}
 	}
+
+	view.change( writer => {
+		writer.removeClass( 'ck-placeholder', element );
+		writer.removeAttribute( 'data-placeholder', element );
+	} );
 }
 }
 
 
 // Updates all placeholders of given document.
 // Updates all placeholders of given document.
 //
 //
 // @private
 // @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 ) {
 	for ( const [ element, checkFunction ] of placeholders ) {
-		updateSinglePlaceholder( element, checkFunction );
+		updateSinglePlaceholder( view, element, checkFunction );
 	}
 	}
 }
 }
 
 
 // Updates placeholder class of given element.
 // Updates placeholder class of given element.
 //
 //
 // @private
 // @private
+// @param {module:engine/view/view~View} view
 // @param {module:engine/view/element~Element} element
 // @param {module:engine/view/element~Element} element
 // @param {Function} checkFunction
 // @param {Function} checkFunction
-function updateSinglePlaceholder( element, checkFunction ) {
+function updateSinglePlaceholder( view, element, checkFunction ) {
 	const document = element.document;
 	const document = element.document;
 
 
 	// Element was removed from 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 is provided and returns false - remove placeholder.
 	if ( checkFunction && !checkFunction() ) {
 	if ( checkFunction && !checkFunction() ) {
-		element.removeClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.removeClass( 'ck-placeholder', element );
+		} );
 
 
 		return;
 		return;
 	}
 	}
@@ -116,15 +116,21 @@ function updateSinglePlaceholder( element, checkFunction ) {
 
 
 	// If element is empty and editor is blurred.
 	// If element is empty and editor is blurred.
 	if ( !document.isFocused && isEmptyish ) {
 	if ( !document.isFocused && isEmptyish ) {
-		element.addClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.addClass( 'ck-placeholder', element );
+		} );
 
 
 		return;
 		return;
 	}
 	}
 
 
 	// It there are no child elements and selection is not placed inside element.
 	// It there are no child elements and selection is not placed inside element.
 	if ( isEmptyish && anchor && anchor.parent !== element ) {
 	if ( isEmptyish && anchor && anchor.parent !== element ) {
-		element.addClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.addClass( 'ck-placeholder', element );
+		} );
 	} else {
 	} else {
-		element.removeClass( 'ck-placeholder' );
+		view.change( writer => {
+			writer.removeClass( 'ck-placeholder', element );
+		} );
 	}
 	}
 }
 }

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

@@ -52,7 +52,7 @@ export default class RootEditableElement extends EditableElement {
 	}
 	}
 
 
 	set rootName( rootName ) {
 	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.
  * Class representing selection in tree view.
  *
  *
  * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using
  * 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
  * That method create copies of provided ranges and store those copies internally. Further modifications to passed
  * ranges will not change selection's state.
  * ranges will not change selection's state.
  * Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges},
  * 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 = '';
 		this._fakeSelectionLabel = '';
 
 
 		if ( selectable ) {
 		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`.
 	 * Returns true if selection instance is marked as `fake`.
 	 *
 	 *
-	 * @see #setFake
+	 * @see #_setFake
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	get isFake() {
 	get isFake() {
@@ -134,7 +114,7 @@ export default class Selection {
 	/**
 	/**
 	 * Returns fake selection label.
 	 * Returns fake selection label.
 	 *
 	 *
-	 * @see #setFake
+	 * @see #_setFake
 	 * @returns {String}
 	 * @returns {String}
 	 */
 	 */
 	get fakeSelectionLabel() {
 	get fakeSelectionLabel() {
@@ -389,6 +369,25 @@ export default class Selection {
 		return true;
 		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.
 	 * Removes all ranges that were added to the selection.
 	 *
 	 *
@@ -429,12 +428,13 @@ export default class Selection {
 	 *
 	 *
 	 * 		// Removes all ranges.
 	 * 		// Removes all ranges.
 	 *		selection.setTo( null );
 	 *		selection.setTo( null );
-
+	 *
+	 * @protected
 	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
 	 * @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
 	 * 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]
 	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
 	 */
 	 */
-	setTo( selectable, backwardSelectionOrOffset ) {
+	_setTo( selectable, backwardSelectionOrOffset ) {
 		if ( selectable === null ) {
 		if ( selectable === null ) {
 			this._removeAllRanges();
 			this._removeAllRanges();
 		} else if ( selectable instanceof Selection ) {
 		} 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.
 	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
 	 *
 	 *
+	 * @protected
 	 * @fires change
 	 * @fires change
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
 	 * @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
 	 * @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}.
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	 */
-	setFocus( itemOrPosition, offset ) {
+	_setFocus( itemOrPosition, offset ) {
 		if ( this.anchor === null ) {
 		if ( this.anchor === null ) {
 			/**
 			/**
 			 * Cannot set selection focus if there are no ranges in selection.
 			 * 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.
  * 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.
  * 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.
 // 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
- */

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1087 - 557
packages/ckeditor5-engine/src/view/writer.js


+ 2 - 2
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -264,7 +264,7 @@ describe( 'DataController', () => {
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 			setData( model, '<paragraph>foo<$text bold="true">bar</$text></paragraph>' );
 
 
 			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
-			downcastAttributeToElement( 'bold', { view: 'strong' } )( data.downcastDispatcher );
+			downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher );
 
 
 			expect( data.get() ).to.equal( '<p>foo<strong>bar</strong></p>' );
 			expect( data.get() ).to.equal( '<p>foo<strong>bar</strong></p>' );
 		} );
 		} );
@@ -277,7 +277,7 @@ describe( 'DataController', () => {
 			setData( model, 'Bar', { rootName: 'title' } );
 			setData( model, 'Bar', { rootName: 'title' } );
 
 
 			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
 			downcastElementToElement( { model: 'paragraph', view: 'p' } )( data.downcastDispatcher );
-			downcastAttributeToElement( 'bold', { view: 'strong' } )( data.downcastDispatcher );
+			downcastAttributeToElement( { model: 'bold', view: 'strong' } )( data.downcastDispatcher );
 
 
 			expect( data.get() ).to.equal( '<p>foo</p>' );
 			expect( data.get() ).to.equal( '<p>foo</p>' );
 			expect( data.get( 'main' ) ).to.equal( '<p>foo</p>' );
 			expect( data.get( 'main' ) ).to.equal( '<p>foo</p>' );

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

+ 495 - 0
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -6,6 +6,18 @@
 import Conversion from '../../src/conversion/conversion';
 import Conversion from '../../src/conversion/conversion';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 
+import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
+
+import { convertText, convertToModelFragment } from '../../src/conversion/upcast-converters';
+
+import EditingController from '../../src/controller/editingcontroller';
+
+import Model from '../../src/model/model';
+import ModelRange from '../../src/model/range';
+
+import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view';
+import { stringify as modelStringify } from '../../src/dev-utils/model';
+
 describe( 'Conversion', () => {
 describe( 'Conversion', () => {
 	let conversion, dispA, dispB;
 	let conversion, dispA, dispB;
 
 
@@ -70,4 +82,487 @@ describe( 'Conversion', () => {
 			expect( helperB.calledWithExactly( dispB ) ).to.be.true;
 			expect( helperB.calledWithExactly( dispB ) ).to.be.true;
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'converters', () => {
+		let viewDispatcher, model, schema, conversion, modelRoot, viewRoot;
+
+		beforeEach( () => {
+			model = new Model();
+			const controller = new EditingController( model );
+
+			const modelDoc = model.document;
+			modelRoot = modelDoc.createRoot();
+
+			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';
+
+			schema = model.schema;
+
+			schema.extend( '$text', {
+				allowAttributes: [ 'bold' ]
+			} );
+
+			schema.register( 'paragraph', {
+				inheritAllFrom: '$block'
+			} );
+
+			viewDispatcher = new UpcastDispatcher( model, { schema } );
+			viewDispatcher.on( 'text', convertText() );
+			viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+			viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+
+			conversion = new Conversion();
+			conversion.register( 'upcast', [ viewDispatcher ] );
+			conversion.register( 'downcast', [ controller.downcastDispatcher ] );
+		} );
+
+		describe( 'elementToElement', () => {
+			it( 'config.view is a string', () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+				test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
+			} );
+
+			it( 'config.priority is defined', () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+				conversion.elementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
+
+				test( '<div>Foo</div>', '<paragraph>Foo</paragraph>' );
+				test( '<p>Foo</p>', '<paragraph>Foo</paragraph>', '<div>Foo</div>' );
+			} );
+
+			it( 'config.view is an object', () => {
+				schema.register( 'fancyParagraph', {
+					inheritAllFrom: 'paragraph'
+				} );
+
+				conversion.elementToElement( {
+					model: 'fancyParagraph',
+					view: {
+						name: 'p',
+						class: 'fancy'
+					}
+				} );
+
+				test( '<p class="fancy">Foo</p>', '<fancyParagraph>Foo</fancyParagraph>' );
+			} );
+
+			it( 'config.view is an object with upcastAlso defined', () => {
+				conversion.elementToElement( {
+					model: 'paragraph',
+					view: 'p',
+					upcastAlso: [
+						'div',
+						{
+							// Any element with `display: block` style.
+							style: {
+								display: 'block'
+							}
+						}
+					]
+				} );
+
+				test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
+				test( '<div>Foo</div>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
+				test( '<span style="display:block">Foo</span>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
+			} );
+
+			it( 'upcastAlso given as a function', () => {
+				schema.register( 'heading', {
+					inheritAllFrom: '$block'
+				} );
+
+				conversion.elementToElement( {
+					model: 'heading',
+					view: 'h2',
+					upcastAlso: viewElement => {
+						const fontSize = viewElement.getStyle( 'font-size' );
+
+						if ( !fontSize ) {
+							return null;
+						}
+
+						const match = fontSize.match( /(\d+)\s*px/ );
+
+						if ( !match ) {
+							return null;
+						}
+
+						const size = Number( match[ 1 ] );
+
+						if ( size >= 26 ) {
+							return { name: true, style: [ 'font-size' ] };
+						}
+
+						return null;
+					}
+				} );
+
+				conversion.elementToElement( {
+					model: 'paragraph',
+					view: 'p'
+				} );
+
+				test( '<p></p>', '<paragraph></paragraph>' );
+				test( '<p style="font-size:20px"></p>', '<paragraph></paragraph>', '<p></p>' );
+
+				test( '<h2></h2>', '<heading></heading>' );
+				test( '<p style="font-size:26px"></p>', '<heading></heading>', '<h2></h2>' );
+			} );
+		} );
+
+		describe( 'attributeToElement', () => {
+			beforeEach( () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+			} );
+
+			it( 'config.view is a string', () => {
+				conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+
+				test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
+			} );
+
+			it( 'config.priority is defined', () => {
+				conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+				conversion.attributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
+
+				test( '<p><b>Foo</b></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>' );
+				test( '<p><strong>Foo</strong></p>', '<paragraph><$text bold="true">Foo</$text></paragraph>', '<p><b>Foo</b></p>' );
+			} );
+
+			it( 'config.view is an object', () => {
+				conversion.attributeToElement( {
+					model: 'bold',
+					view: {
+						name: 'span',
+						class: 'bold'
+					}
+				} );
+
+				test( '<p><span class="bold">Foo</span> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
+			} );
+
+			it( 'config.view is an object with upcastAlso defined', () => {
+				conversion.attributeToElement( {
+					model: 'bold',
+					view: 'strong',
+					upcastAlso: [
+						'b',
+						{
+							name: 'span',
+							class: 'bold'
+						},
+						{
+							name: 'span',
+							style: {
+								'font-weight': 'bold'
+							}
+						},
+						viewElement => {
+							const fontWeight = viewElement.getStyle( 'font-weight' );
+
+							if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
+								return {
+									name: true,
+									style: [ 'font-weight' ]
+								};
+							}
+						}
+					]
+				} );
+
+				test(
+					'<p><strong>Foo</strong></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>'
+				);
+
+				test(
+					'<p><b>Foo</b></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+
+				test(
+					'<p><span class="bold">Foo</span></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+
+				test(
+					'<p><span style="font-weight: bold;">Foo</span></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+
+				test(
+					'<p><span style="font-weight: 500;">Foo</span></p>',
+					'<paragraph>Foo</paragraph>',
+					'<p>Foo</p>'
+				);
+
+				test(
+					'<p><span style="font-weight: 600;">Foo</span></p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+			} );
+
+			it( 'model attribute value is enumerable', () => {
+				schema.extend( '$text', {
+					allowAttributes: [ 'fontSize' ]
+				} );
+
+				conversion.attributeToElement( {
+					model: {
+						key: 'fontSize',
+						values: [ 'big', 'small' ]
+					},
+					view: {
+						big: {
+							name: 'span',
+							style: {
+								'font-size': '1.2em'
+							}
+						},
+						small: {
+							name: 'span',
+							style: {
+								'font-size': '0.8em'
+							}
+						}
+					},
+					upcastAlso: {
+						big: viewElement => {
+							const fontSize = viewElement.getStyle( 'font-size' );
+
+							if ( !fontSize ) {
+								return null;
+							}
+
+							const match = fontSize.match( /(\d+)\s*px/ );
+
+							if ( !match ) {
+								return null;
+							}
+
+							const size = Number( match[ 1 ] );
+
+							if ( viewElement.is( 'span' ) && size > 10 ) {
+								return { name: true, style: [ 'font-size' ] };
+							}
+
+							return null;
+						},
+						small: viewElement => {
+							const fontSize = viewElement.getStyle( 'font-size' );
+
+							if ( !fontSize ) {
+								return null;
+							}
+
+							const match = fontSize.match( /(\d+)\s*px/ );
+
+							if ( !match ) {
+								return null;
+							}
+
+							const size = Number( match[ 1 ] );
+
+							if ( viewElement.is( 'span' ) && size < 10 ) {
+								return { name: true, style: [ 'font-size' ] };
+							}
+
+							return null;
+						}
+					}
+				} );
+
+				test(
+					'<p><span style="font-size:1.2em">Foo</span> bar</p>',
+					'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>'
+				);
+
+				test(
+					'<p><span style="font-size:12px">Foo</span> bar</p>',
+					'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>',
+					'<p><span style="font-size:1.2em">Foo</span> bar</p>'
+				);
+
+				test(
+					'<p><span style="font-size:0.8em">Foo</span> bar</p>',
+					'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
+				);
+
+				test(
+					'<p><span style="font-size:8px">Foo</span> bar</p>',
+					'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>',
+					'<p><span style="font-size:0.8em">Foo</span> bar</p>'
+				);
+
+				test(
+					'<p><span style="font-size:10px">Foo</span> bar</p>',
+					'<paragraph>Foo bar</paragraph>',
+					'<p>Foo bar</p>'
+				);
+			} );
+		} );
+
+		describe( 'attributeToAttribute', () => {
+			beforeEach( () => {
+				conversion.elementToElement( { model: 'image', view: 'img' } );
+
+				schema.register( 'image', {
+					inheritAllFrom: '$block',
+				} );
+			} );
+
+			it( 'config.view and config.model are strings', () => {
+				schema.extend( 'image', {
+					allowAttributes: [ 'source' ]
+				} );
+
+				conversion.attributeToAttribute( { model: 'source', view: 'src' } );
+
+				test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
+			} );
+
+			it( 'config.view and config.model are objects', () => {
+				schema.extend( 'image', {
+					allowAttributes: [ 'aside' ]
+				} );
+
+				conversion.attributeToAttribute( {
+					model: {
+						name: 'image',
+						key: 'aside',
+						values: [ 'aside' ]
+					},
+					view: {
+						aside: {
+							name: 'img',
+							key: 'class',
+							value: [ 'aside', 'half-size' ]
+						}
+					}
+				} );
+
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+				test( '<img class="aside half-size"></img>', '<image aside="aside"></image>' );
+				test( '<p class="aside half-size"></p>', '<paragraph></paragraph>', '<p></p>' );
+			} );
+
+			it( 'config.view and config.model are objects - convert to style attribute', () => {
+				schema.extend( 'image', {
+					allowAttributes: [ 'aside' ]
+				} );
+
+				conversion.attributeToAttribute( {
+					model: {
+						name: 'image',
+						key: 'aside',
+						values: [ 'aside' ]
+					},
+					view: {
+						aside: {
+							name: 'img',
+							key: 'style',
+							value: {
+								float: 'right',
+								width: '50%',
+								margin: '5px'
+							}
+						}
+					}
+				} );
+
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+				test( '<img style="float:right;margin:5px;width:50%"></img>', '<image aside="aside"></image>' );
+				test( '<p style="float:right;margin:5px;width:50%"></p>', '<paragraph></paragraph>', '<p></p>' );
+			} );
+
+			it( 'config is an array with upcastAlso defined', () => {
+				conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+				schema.extend( 'paragraph', {
+					allowAttributes: [ 'align' ]
+				} );
+
+				conversion.attributeToAttribute( {
+					model: {
+						key: 'align',
+						values: [ 'right', 'center' ]
+					},
+					view: {
+						right: {
+							key: 'class',
+							value: 'align-right'
+						},
+						center: {
+							key: 'class',
+							value: 'align-center'
+						}
+					},
+					upcastAlso: {
+						right: {
+							style: {
+								'text-align': 'right'
+							}
+						},
+						center: {
+							style: {
+								'text-align': 'center'
+							}
+						}
+					}
+				} );
+
+				test(
+					'<p class="align-right">Foo</p>',
+					'<paragraph align="right">Foo</paragraph>'
+				);
+
+				test(
+					'<p style="text-align:right">Foo</p>',
+					'<paragraph align="right">Foo</paragraph>',
+					'<p class="align-right">Foo</p>'
+				);
+
+				test(
+					'<p class="align-center">Foo</p>',
+					'<paragraph align="center">Foo</paragraph>'
+				);
+
+				test(
+					'<p style="text-align:center">Foo</p>',
+					'<paragraph align="center">Foo</paragraph>',
+					'<p class="align-center">Foo</p>'
+				);
+			} );
+		} );
+
+		function test( input, expectedModel, expectedView = null ) {
+			loadData( input );
+
+			expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel );
+			expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView || input );
+		}
+
+		function loadData( input ) {
+			const parsedView = viewParse( input );
+			let convertedModel;
+
+			model.change( writer => {
+				convertedModel = viewDispatcher.convert( parsedView, writer );
+			} );
+
+			model.change( writer => {
+				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );
+				writer.insert( convertedModel, modelRoot, 0 );
+			} );
+		}
+	} );
 } );
 } );

+ 190 - 235
packages/ckeditor5-engine/tests/conversion/downcast-converters.js

@@ -15,7 +15,6 @@ import ModelPosition from '../../src/model/position';
 
 
 import ViewElement from '../../src/view/element';
 import ViewElement from '../../src/view/element';
 import ViewContainerElement from '../../src/view/containerelement';
 import ViewContainerElement from '../../src/view/containerelement';
-import ViewAttributeElement from '../../src/view/attributeelement';
 import ViewUIElement from '../../src/view/uielement';
 import ViewUIElement from '../../src/view/uielement';
 import ViewText from '../../src/view/text';
 import ViewText from '../../src/view/text';
 
 
@@ -39,9 +38,9 @@ describe( 'downcast-helpers', () => {
 
 
 		// Set name of view root the same as dom root.
 		// Set name of view root the same as dom root.
 		// This is a mock of attaching view root to 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 = new Conversion();
 		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
 		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
@@ -62,7 +61,7 @@ describe( 'downcast-helpers', () => {
 
 
 		it( 'can be overwritten using priority', () => {
 		it( 'can be overwritten using priority', () => {
 			const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
 			const helperA = downcastElementToElement( { model: 'paragraph', view: 'p' } );
-			const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo' }, 'high' );
+			const helperB = downcastElementToElement( { model: 'paragraph', view: 'foo', priority: 'high' } );
 
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
 
@@ -73,21 +72,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<foo></foo>' );
 			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', () => {
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastElementToElement( {
 			const helper = downcastElementToElement( {
 				model: 'fancyParagraph',
 				model: 'fancyParagraph',
@@ -109,7 +93,7 @@ describe( 'downcast-helpers', () => {
 		it( 'config.view is a function', () => {
 		it( 'config.view is a function', () => {
 			const helper = downcastElementToElement( {
 			const helper = downcastElementToElement( {
 				model: 'heading',
 				model: 'heading',
-				view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+				view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
 			} );
 			} );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
@@ -124,7 +108,7 @@ describe( 'downcast-helpers', () => {
 
 
 	describe( 'downcastAttributeToElement', () => {
 	describe( 'downcastAttributeToElement', () => {
 		it( 'config.view is a string', () => {
 		it( 'config.view is a string', () => {
-			const helper = downcastAttributeToElement( 'bold', { view: 'strong' } );
+			const helper = downcastAttributeToElement( { model: 'bold', view: 'strong' } );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
 
 
@@ -136,8 +120,8 @@ describe( 'downcast-helpers', () => {
 		} );
 		} );
 
 
 		it( 'can be overwritten using priority', () => {
 		it( 'can be overwritten using priority', () => {
-			const helperA = downcastAttributeToElement( 'bold', { view: 'strong' } );
-			const helperB = downcastAttributeToElement( 'bold', { view: 'b' }, 'high' );
+			const helperA = downcastAttributeToElement( { model: 'bold', view: 'strong' } );
+			const helperB = downcastAttributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
 
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
 
@@ -148,82 +132,45 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<b>foo</b>' );
 			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', () => {
 		it( 'config.view is a view element definition', () => {
-			const helper = downcastAttributeToElement( 'bold', {
+			const helper = downcastAttributeToElement( {
+				model: 'invert',
 				view: {
 				view: {
 					name: 'span',
 					name: 'span',
-					class: 'bold'
+					class: [ 'font-light', 'bg-dark' ]
 				}
 				}
 			} );
 			} );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.insertText( 'foo', { bold: true }, modelRoot, 0 );
+				writer.insertText( 'foo', { invert: true }, modelRoot, 0 );
 			} );
 			} );
 
 
-			expectResult( '<span class="bold">foo</span>' );
+			expectResult( '<span class="bg-dark font-light">foo</span>' );
 		} );
 		} );
 
 
-		it( 'config.view is a view element definition, model attribute value specified', () => {
-			const helper = downcastAttributeToElement( 'styled', {
-				model: 'dark',
+		it( 'model attribute value is enum', () => {
+			const helper = downcastAttributeToElement( {
+				model: {
+					key: 'fontSize',
+					values: [ 'big', 'small' ]
+				},
 				view: {
 				view: {
-					name: 'span',
-					class: [ 'styled', 'styled-dark' ]
-				}
-			} );
-
-			conversion.for( 'downcast' ).add( helper );
-
-			model.change( writer => {
-				writer.insertText( 'foo', { styled: 'dark' }, modelRoot, 0 );
-			} );
-
-			expectResult( '<span class="styled styled-dark">foo</span>' );
-
-			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
-			} );
-
-			expectResult( 'foo' );
-		} );
-
-		it( 'multiple config items', () => {
-			const helper = downcastAttributeToElement( 'fontSize', [
-				{
-					model: 'big',
-					view: {
+					big: {
 						name: 'span',
 						name: 'span',
 						style: {
 						style: {
 							'font-size': '1.2em'
 							'font-size': '1.2em'
 						}
 						}
-					}
-				},
-				{
-					model: 'small',
-					view: {
+					},
+					small: {
 						name: 'span',
 						name: 'span',
 						style: {
 						style: {
 							'font-size': '0.8em'
 							'font-size': '0.8em'
 						}
 						}
 					}
 					}
 				}
 				}
-			] );
+			} );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
 
 
@@ -247,8 +194,11 @@ describe( 'downcast-helpers', () => {
 		} );
 		} );
 
 
 		it( 'config.view is a function', () => {
 		it( 'config.view is a function', () => {
-			const helper = downcastAttributeToElement( 'bold', {
-				view: attributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + attributeValue } )
+			const helper = downcastAttributeToElement( {
+				model: 'bold',
+				view: ( modelAttributeValue, viewWriter ) => {
+					return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
+				}
 			} );
 			} );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
@@ -266,33 +216,27 @@ describe( 'downcast-helpers', () => {
 			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
 			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'image', view: 'img' } ) );
 		} );
 		} );
 
 
-		it( 'config not set', () => {
-			const helper = downcastAttributeToAttribute( 'src' );
+		it( 'config.view is a string', () => {
+			const helper = downcastAttributeToAttribute( { model: 'source', view: 'src' } );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.insertElement( 'image', { src: 'foo.jpg' }, modelRoot, 0 );
+				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
 			} );
 			} );
 
 
 			expectResult( '<img src="foo.jpg"></img>' );
 			expectResult( '<img src="foo.jpg"></img>' );
-		} );
-
-		it( 'config.view is a string', () => {
-			const helper = downcastAttributeToAttribute( 'source', { view: 'src' } );
-
-			conversion.for( 'downcast' ).add( helper );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
+				writer.removeAttribute( 'source', modelRoot.getChild( 0 ) );
 			} );
 			} );
 
 
-			expectResult( '<img src="foo.jpg"></img>' );
+			expectResult( '<img></img>' );
 		} );
 		} );
 
 
 		it( 'can be overwritten using priority', () => {
 		it( 'can be overwritten using priority', () => {
-			const helperA = downcastAttributeToAttribute( 'source', { view: 'href' } );
-			const helperB = downcastAttributeToAttribute( 'source', { view: 'src' }, 'high' );
+			const helperA = downcastAttributeToAttribute( { model: 'source', view: 'href' } );
+			const helperB = downcastAttributeToAttribute( { model: 'source', view: 'src', priority: 'high' } );
 
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
 
@@ -303,83 +247,121 @@ describe( 'downcast-helpers', () => {
 			expectResult( '<img src="foo.jpg"></img>' );
 			expectResult( '<img src="foo.jpg"></img>' );
 		} );
 		} );
 
 
-		it( 'config.view is an object', () => {
-			const helper = downcastAttributeToAttribute( 'stylish', { view: { key: 'class', value: 'styled' } } );
+		it( 'model element name specified', () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) );
+
+			const helper = downcastAttributeToAttribute( {
+				model: {
+					name: 'image',
+					key: 'source'
+				},
+				view: 'src'
+			} );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.insertElement( 'image', { stylish: true }, modelRoot, 0 );
+				writer.insertElement( 'image', { source: 'foo.jpg' }, modelRoot, 0 );
+			} );
+
+			expectResult( '<img src="foo.jpg"></img>' );
+
+			model.change( writer => {
+				writer.rename( modelRoot.getChild( 0 ), 'paragraph' );
 			} );
 			} );
 
 
-			expectResult( '<img class="styled"></img>' );
+			expectResult( '<p></p>' );
 		} );
 		} );
 
 
-		it( 'config.view is an object, model attribute value specified', () => {
-			const helper = downcastAttributeToAttribute( 'styled', {
-				model: 'dark',
+		it( 'config.view is an object, model attribute value is enum', () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) );
+
+			const helper = downcastAttributeToAttribute( {
+				model: {
+					key: 'styled',
+					values: [ 'dark', 'light' ]
+				},
 				view: {
 				view: {
-					key: 'class',
-					value: 'styled-dark styled'
+					dark: {
+						key: 'class',
+						value: [ 'styled', 'styled-dark' ]
+					},
+					light: {
+						key: 'class',
+						value: [ 'styled', 'styled-light' ]
+					}
 				}
 				}
 			} );
 			} );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+				writer.insertElement( 'paragraph', { styled: 'dark' }, modelRoot, 0 );
 			} );
 			} );
 
 
-			expectResult( '<img class="styled styled-dark"></img>' );
+			expectResult( '<p class="styled styled-dark"></p>' );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+				writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
 			} );
 			} );
 
 
-			expectResult( '<img></img>' );
+			expectResult( '<p class="styled styled-light"></p>' );
+
+			model.change( writer => {
+				writer.removeAttribute( 'styled', modelRoot.getChild( 0 ) );
+			} );
+
+			expectResult( '<p></p>' );
 		} );
 		} );
 
 
-		it( 'multiple config items', () => {
-			const helper = downcastAttributeToAttribute( 'styled', [
-				{
-					model: 'dark',
-					view: {
-						key: 'class',
-						value: 'styled-dark'
-					}
+		it( 'config.view is an object, model attribute value is enum, view has style', () => {
+			conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'paragraph', view: 'p' } ) );
+
+			const helper = downcastAttributeToAttribute( {
+				model: {
+					key: 'align',
+					values: [ 'right', 'center' ]
 				},
 				},
-				{
-					model: 'light',
-					view: {
-						key: 'class',
-						value: 'styled-light'
+				view: {
+					right: {
+						key: 'style',
+						value: {
+							'text-align': 'right'
+						}
+					},
+					center: {
+						key: 'style',
+						value: {
+							'text-align': 'center'
+						}
 					}
 					}
 				}
 				}
-			] );
+			} );
 
 
 			conversion.for( 'downcast' ).add( helper );
 			conversion.for( 'downcast' ).add( helper );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.insertElement( 'image', { styled: 'dark' }, modelRoot, 0 );
+				writer.insertElement( 'paragraph', { align: 'right' }, modelRoot, 0 );
 			} );
 			} );
 
 
-			expectResult( '<img class="styled-dark"></img>' );
+			expectResult( '<p style="text-align:right"></p>' );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setAttribute( 'styled', 'light', modelRoot.getChild( 0 ) );
+				writer.setAttribute( 'align', 'center', modelRoot.getChild( 0 ) );
 			} );
 			} );
 
 
-			expectResult( '<img class="styled-light"></img>' );
+			expectResult( '<p style="text-align:center"></p>' );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.setAttribute( 'styled', 'xyz', modelRoot.getChild( 0 ) );
+				writer.removeAttribute( 'align', modelRoot.getChild( 0 ) );
 			} );
 			} );
 
 
-			expectResult( '<img></img>' );
+			expectResult( '<p></p>' );
 		} );
 		} );
 
 
 		it( 'config.view is a function', () => {
 		it( 'config.view is a function', () => {
-			const helper = downcastAttributeToAttribute( 'styled', {
+			const helper = downcastAttributeToAttribute( {
+				model: 'styled',
 				view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
 				view: attributeValue => ( { key: 'class', value: 'styled-' + attributeValue } )
 			} );
 			} );
 
 
@@ -409,7 +391,7 @@ describe( 'downcast-helpers', () => {
 
 
 		it( 'can be overwritten using priority', () => {
 		it( 'can be overwritten using priority', () => {
 			const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
 			const helperA = downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
-			const helperB = downcastMarkerToElement( { model: 'search', view: 'search' }, 'high' );
+			const helperB = downcastMarkerToElement( { model: 'search', view: 'search', priority: 'high' } );
 
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
 
@@ -421,22 +403,6 @@ describe( 'downcast-helpers', () => {
 			expectResult( 'f<search></search>o<search></search>o' );
 			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', () => {
 		it( 'config.view is a view element definition', () => {
 			const helper = downcastMarkerToElement( {
 			const helper = downcastMarkerToElement( {
 				model: 'search',
 				model: 'search',
@@ -461,8 +427,8 @@ describe( 'downcast-helpers', () => {
 		it( 'config.view is a function', () => {
 		it( 'config.view is a function', () => {
 			const helper = downcastMarkerToElement( {
 			const helper = downcastMarkerToElement( {
 				model: 'search',
 				model: 'search',
-				view: data => {
-					return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
+				view: ( data, viewWriter ) => {
+					return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } );
 				}
 				}
 			} );
 			} );
 
 
@@ -493,7 +459,7 @@ describe( 'downcast-helpers', () => {
 
 
 		it( 'can be overwritten using priority', () => {
 		it( 'can be overwritten using priority', () => {
 			const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
 			const helperA = downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
-			const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' } }, 'high' );
+			const helperB = downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' }, priority: 'high' } );
 
 
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 			conversion.for( 'downcast' ).add( helperA ).add( helperB );
 
 
@@ -543,14 +509,20 @@ describe( 'downcast-converters', () => {
 
 
 		controller = new EditingController( model );
 		controller = new EditingController( model );
 
 
-		viewRoot = controller.view.getRoot();
+		viewRoot = controller.view.document.getRoot();
 		// Set name of view root the same as dom root.
 		// Set name of view root the same as dom root.
 		// This is a mock of attaching view root to 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 = controller.downcastDispatcher;
 
 
-		dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) );
+		dispatcher.on(
+			'insert:paragraph',
+			insertElement(
+				( modelItem, viewWriter ) => viewWriter.createContainerElement( 'p' )
+			)
+		);
+
 		dispatcher.on( 'attribute:class', changeAttribute() );
 		dispatcher.on( 'attribute:class', changeAttribute() );
 
 
 		modelRootStart = ModelPosition.createAt( modelRoot, 0 );
 		modelRootStart = ModelPosition.createAt( modelRoot, 0 );
@@ -607,8 +579,8 @@ describe( 'downcast-converters', () => {
 		} );
 		} );
 
 
 		it( 'should be possible to override it', () => {
 		it( 'should be possible to override it', () => {
-			dispatcher.on( 'insert:$text', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'insert' );
+			dispatcher.on( 'insert:$text', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'insert' );
 			}, { priority: 'high' } );
 			}, { priority: 'high' } );
 
 
 			model.change( writer => {
 			model.change( writer => {
@@ -620,7 +592,7 @@ describe( 'downcast-converters', () => {
 	} );
 	} );
 
 
 	describe( 'insertElement', () => {
 	describe( 'insertElement', () => {
-		it( 'should convert element insertion in model to and map positions for future converting', () => {
+		it( 'should convert element insertion in model', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) );
 
 
 			model.change( writer => {
 			model.change( writer => {
@@ -630,26 +602,16 @@ describe( 'downcast-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 		} );
 		} );
 
 
-		it( 'should take view element function generator as a parameter', () => {
-			const elementGenerator = ( modelItem, consumable ) => {
-				if ( consumable.consume( modelItem, 'attribute:nice' ) ) {
-					return new ViewContainerElement( 'div' );
-				}
-
-				// Test if default converter will be fired for paragraph, if `null` is returned and consumable was not consumed.
-				return null;
-			};
-
-			dispatcher.on( 'insert:paragraph', insertElement( elementGenerator ), { priority: 'high' } );
+		it( 'should not convert if creator returned null', () => {
+			dispatcher.on( 'insert:div', insertElement( () => null ) );
 
 
-			const niceP = new ModelElement( 'paragraph', { nice: true }, new ModelText( 'foo' ) );
-			const badP = new ModelElement( 'paragraph', null, new ModelText( 'bar' ) );
+			const modelElement = new ModelElement( 'div' );
 
 
 			model.change( writer => {
 			model.change( writer => {
-				writer.insert( [ niceP, badP ], modelRootStart );
+				writer.insert( modelElement, modelRootStart );
 			} );
 			} );
 
 
-			expect( viewToString( viewRoot ) ).to.equal( '<div><div>foo</div><p>bar</p></div>' );
+			expect( viewToString( viewRoot ) ).to.equal( '<div></div>' );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -685,7 +647,7 @@ describe( 'downcast-converters', () => {
 				return { key: 'class', value };
 				return { key: 'class', value };
 			};
 			};
 
 
-			dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) );
+			dispatcher.on( 'insert:div', insertElement( ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'div' ) ) );
 			dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
 			dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) );
 
 
 			const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
 			const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) );
@@ -713,8 +675,8 @@ describe( 'downcast-converters', () => {
 		it( 'should be possible to override setAttribute', () => {
 		it( 'should be possible to override setAttribute', () => {
 			const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
 			const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
 
 
-			dispatcher.on( 'attribute:class', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'attribute:class' );
+			dispatcher.on( 'attribute:class', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'attribute:class' );
 			}, { priority: 'high' } );
 			}, { priority: 'high' } );
 
 
 			model.change( writer => {
 			model.change( writer => {
@@ -729,9 +691,9 @@ describe( 'downcast-converters', () => {
 	describe( 'wrap', () => {
 	describe( 'wrap', () => {
 		it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => {
 		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 modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' );
 
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
 				writer.insert( modelElement, modelRootStart );
@@ -749,9 +711,9 @@ describe( 'downcast-converters', () => {
 		it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => {
 		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 modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) );
 
 
-			const elementGenerator = value => {
-				if ( value == 'bold' ) {
-					return new ViewAttributeElement( 'b' );
+			const elementGenerator = ( modelAttributeValue, viewWriter ) => {
+				if ( modelAttributeValue == 'bold' ) {
+					return viewWriter.createAttributeElement( 'b' );
 				}
 				}
 			};
 			};
 
 
@@ -777,7 +739,9 @@ describe( 'downcast-converters', () => {
 				new ModelText( 'x' )
 				new ModelText( 'x' )
 			] );
 			] );
 
 
-			const elementGenerator = href => new ViewAttributeElement( 'a', { href } );
+			const elementGenerator = ( modelAttributeValue, viewWriter ) => {
+				return viewWriter.createAttributeElement( 'a', { href: modelAttributeValue } );
+			};
 
 
 			dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
 			dispatcher.on( 'attribute:link', wrap( elementGenerator ) );
 
 
@@ -797,9 +761,9 @@ describe( 'downcast-converters', () => {
 
 
 		it( 'should support unicode', () => {
 		it( 'should support unicode', () => {
 			const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
 			const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] );
-			const viewB = new ViewAttributeElement( 'b' );
+			const creator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' );
 
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
+			dispatcher.on( 'attribute:bold', wrap( creator ) );
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
 				writer.insert( modelElement, modelRootStart );
@@ -816,18 +780,20 @@ describe( 'downcast-converters', () => {
 
 
 		it( 'should be possible to override wrap', () => {
 		it( 'should be possible to override wrap', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) );
-			const viewB = new ViewAttributeElement( 'b' );
 
 
-			dispatcher.on( 'attribute:bold', wrap( viewB ) );
-			dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'attribute:bold' );
-			}, { priority: 'high' } );
+			dispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'b' ) ) );
+
+			dispatcher.on(
+				'attribute:bold',
+				wrap( ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' ) ),
+				{ priority: 'high' }
+			);
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
 				writer.insert( modelElement, modelRootStart );
 			} );
 			} );
 
 
-			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p><strong>foobar</strong></p></div>' );
 		} );
 		} );
 
 
 		it( 'should not convert and not consume if creator function returned null', () => {
 		it( 'should not convert and not consume if creator function returned null', () => {
@@ -838,9 +804,9 @@ describe( 'downcast-converters', () => {
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
 			const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { italic: true } ) );
 
 
 			dispatcher.on( 'attribute:italic', wrap( elementGenerator ) );
 			dispatcher.on( 'attribute:italic', wrap( elementGenerator ) );
-			dispatcher.on( 'attribute:italic', ( evt, data, consumable ) => {
-				expect( consumable.test( data.item, 'attribute:italic' ) ).to.be.true;
-			} );
+
+			const spy = sinon.spy();
+			dispatcher.on( 'attribute:italic', spy );
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.insert( modelElement, modelRootStart );
 				writer.insert( modelElement, modelRootStart );
@@ -848,6 +814,7 @@ describe( 'downcast-converters', () => {
 
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 			expect( dispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'attribute:italic:$text' ) ).to.be.true;
+			expect( spy.called ).to.be.true;
 		} );
 		} );
 	} );
 	} );
 
 
@@ -868,30 +835,11 @@ describe( 'downcast-converters', () => {
 				range = ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 3 );
 				range = ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 3 );
 			} );
 			} );
 
 
-			it( 'should insert and remove ui element - element as a creator', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
-
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
-
-				model.change( writer => {
-					writer.setMarker( 'marker', range );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foo<span class="marker"></span>bar</p></div>' );
-
-				model.change( writer => {
-					writer.removeMarker( 'marker' );
-				} );
-
-				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
-			} );
+			it( 'should insert and remove ui element', () => {
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } );
 
 
-			it( 'should insert and remove ui element - function as a creator', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
-
-				dispatcher.on( 'addMarker:marker', insertUIElement( () => viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( () => viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
+				dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
 
 
 				model.change( writer => {
 				model.change( writer => {
 					writer.setMarker( 'marker', range );
 					writer.setMarker( 'marker', range );
@@ -907,16 +855,19 @@ describe( 'downcast-converters', () => {
 			} );
 			} );
 
 
 			it( 'should not convert if consumable was consumed', () => {
 			it( 'should not convert if consumable was consumed', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
-
 				sinon.spy( dispatcher, 'fire' );
 				sinon.spy( dispatcher, 'fire' );
 
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
-					consumable.consume( data.markerRange, 'addMarker:marker' );
+				dispatcher.on( 'addMarker:marker', insertUIElement(
+					( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } ) )
+				);
+
+				dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
+					conversionApi.consumable.consume( data.markerRange, 'addMarker:marker' );
 				}, { priority: 'high' } );
 				}, { priority: 'high' } );
 
 
-				dispatcher.convertMarkerAdd( 'marker', range );
+				model.change( writer => {
+					writer.setMarker( 'marker', range );
+				} );
 
 
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 				expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
 				expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
@@ -946,10 +897,10 @@ describe( 'downcast-converters', () => {
 			} );
 			} );
 
 
 			it( 'should insert and remove ui element - element as a creator', () => {
 			it( 'should insert and remove ui element - element as a creator', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } );
 
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
+				dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
 
 
 				model.change( writer => {
 				model.change( writer => {
 					writer.setMarker( 'marker', range );
 					writer.setMarker( 'marker', range );
@@ -966,10 +917,10 @@ describe( 'downcast-converters', () => {
 			} );
 			} );
 
 
 			it( 'should insert and remove ui element - function as a creator', () => {
 			it( 'should insert and remove ui element - function as a creator', () => {
-				const viewUi = data => new ViewUIElement( 'span', { 'class': data.markerName } );
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': data.markerName } );
 
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'removeMarker:marker', removeUIElement( viewUi ) );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
+				dispatcher.on( 'removeMarker:marker', removeUIElement( creator ) );
 
 
 				model.change( writer => {
 				model.change( writer => {
 					writer.setMarker( 'marker', range );
 					writer.setMarker( 'marker', range );
@@ -986,12 +937,12 @@ describe( 'downcast-converters', () => {
 			} );
 			} );
 
 
 			it( 'should insert and remove different opening and ending element', () => {
 			it( 'should insert and remove different opening and ending element', () => {
-				function creator( data ) {
+				function creator( data, viewWriter ) {
 					if ( data.isOpening ) {
 					if ( data.isOpening ) {
-						return new ViewUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
+						return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-start': true } );
 					}
 					}
 
 
-					return new ViewUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
+					return viewWriter.createUIElement( 'span', { 'class': data.markerName, 'data-end': true } );
 				}
 				}
 
 
 				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
 				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
@@ -1013,16 +964,18 @@ describe( 'downcast-converters', () => {
 			} );
 			} );
 
 
 			it( 'should not convert if consumable was consumed', () => {
 			it( 'should not convert if consumable was consumed', () => {
-				const viewUi = new ViewUIElement( 'span', { 'class': 'marker' } );
+				const creator = ( data, viewWriter ) => viewWriter.createUIElement( 'span', { 'class': 'marker' } );
 
 
 				sinon.spy( dispatcher, 'fire' );
 				sinon.spy( dispatcher, 'fire' );
 
 
-				dispatcher.on( 'addMarker:marker', insertUIElement( viewUi ) );
-				dispatcher.on( 'addMarker:marker', ( evt, data, consumable ) => {
-					consumable.consume( data.item, 'addMarker:marker' );
+				dispatcher.on( 'addMarker:marker', insertUIElement( creator ) );
+				dispatcher.on( 'addMarker:marker', ( evt, data, conversionApi ) => {
+					conversionApi.consumable.consume( data.item, 'addMarker:marker' );
 				}, { priority: 'high' } );
 				}, { priority: 'high' } );
 
 
-				dispatcher.convertMarkerAdd( 'marker', range );
+				model.change( writer => {
+					writer.setMarker( 'marker', range );
+				} );
 
 
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 				expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 				expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
 				expect( dispatcher.fire.calledWith( 'addMarker:marker' ) );
@@ -1138,7 +1091,9 @@ describe( 'downcast-converters', () => {
 			const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
 			const modelP = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
 			const modelWidget = new ModelElement( 'widget', null, modelP );
 			const modelWidget = new ModelElement( 'widget', null, modelP );
 
 
-			dispatcher.on( 'insert:widget', insertElement( () => new ViewContainerElement( 'widget' ) ) );
+			dispatcher.on( 'insert:widget', insertElement(
+				( modelElement, viewWriter ) => viewWriter.createContainerElement( 'widget' ) )
+			);
 
 
 			model.change( writer => {
 			model.change( writer => {
 				writer.insert( modelWidget, modelRootStart );
 				writer.insert( modelWidget, modelRootStart );
@@ -1341,12 +1296,12 @@ describe( 'downcast-converters', () => {
 				dispatcher.on( 'insert:div', insertElement( () => {
 				dispatcher.on( 'insert:div', insertElement( () => {
 					const viewContainer = new ViewContainerElement( 'div' );
 					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;
 					return viewContainer;
@@ -1418,12 +1373,12 @@ describe( 'downcast-converters', () => {
 				dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) );
 				dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) );
 				dispatcher.on( 'removeMarker:marker2', removeHighlight( () => 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.priority ).to.equal( 10 );
 					expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' );
 					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' );
 					expect( id ).to.equal( 'marker:foo-bar-baz' );
 				} );
 				} );
 
 

+ 82 - 69
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 ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
 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 ViewUIElement from '../../src/view/uielement';
-import { mergeAttributes } from '../../src/view/writer';
 
 
 import Mapper from '../../src/conversion/mapper';
 import Mapper from '../../src/conversion/mapper';
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
 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';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
 
 describe( 'downcast-selection-converters', () => {
 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( () => {
 	beforeEach( () => {
 		model = new Model();
 		model = new Model();
@@ -47,7 +44,8 @@ describe( 'downcast-selection-converters', () => {
 
 
 		model.schema.extend( '$text', { allowIn: '$root' } );
 		model.schema.extend( '$text', { allowIn: '$root' } );
 
 
-		viewDoc = new ViewDocument();
+		view = new View();
+		viewDoc = view.document;
 		viewRoot = createViewRoot( viewDoc );
 		viewRoot = createViewRoot( viewDoc );
 		viewSelection = viewDoc.selection;
 		viewSelection = viewDoc.selection;
 
 
@@ -56,10 +54,12 @@ describe( 'downcast-selection-converters', () => {
 
 
 		highlightDescriptor = { class: 'marker', priority: 1 };
 		highlightDescriptor = { class: 'marker', priority: 1 };
 
 
-		dispatcher = new DowncastDispatcher( model, { mapper, viewSelection } );
+		dispatcher = new DowncastDispatcher( { mapper, viewSelection } );
 
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
-		dispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
+
+		const strongCreator = ( modelAttributeValue, viewWriter ) => viewWriter.createAttributeElement( 'strong' );
+		dispatcher.on( 'attribute:bold', wrap( strongCreator ) );
 
 
 		dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
 		dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
 		dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
 		dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
@@ -72,7 +72,7 @@ describe( 'downcast-selection-converters', () => {
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
-		viewDoc.destroy();
+		view.destroy();
 	} );
 	} );
 
 
 	describe( 'default converters', () => {
 	describe( 'default converters', () => {
@@ -152,8 +152,8 @@ describe( 'downcast-selection-converters', () => {
 			it( 'consumes consumable values properly', () => {
 			it( 'consumes consumable values properly', () => {
 				// Add callback that will fire before default ones.
 				// Add callback that will fire before default ones.
 				// This should prevent default callback doing anything.
 				// This should prevent default callback doing anything.
-				dispatcher.on( 'selection', ( evt, data, consumable ) => {
-					expect( consumable.consume( data.selection, 'selection' ) ).to.be.true;
+				dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+					expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true;
 				}, { priority: 'high' } );
 				}, { priority: 'high' } );
 
 
 				// Similar test case as the first in this suite.
 				// Similar test case as the first in this suite.
@@ -206,11 +206,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 
 				// Convert model to view.
 				// 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.
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal(
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal(
@@ -231,11 +231,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 
 				// Convert model to view.
 				// 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.
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -258,11 +258,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 
 				// Convert model to view.
 				// 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.
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -283,11 +283,11 @@ describe( 'downcast-selection-converters', () => {
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 				viewRoot.removeChildren( 0, viewRoot.childCount );
 
 
 				// Convert model to view.
 				// 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.
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -310,7 +310,9 @@ describe( 'downcast-selection-converters', () => {
 				} );
 				} );
 
 
 				// Convert model to view.
 				// 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.
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -327,13 +329,15 @@ describe( 'downcast-selection-converters', () => {
 				} );
 				} );
 
 
 				// Convert model to view.
 				// 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.
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -350,13 +354,14 @@ describe( 'downcast-selection-converters', () => {
 				} );
 				} );
 
 
 				// Convert model to view.
 				// 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.
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -366,12 +371,12 @@ describe( 'downcast-selection-converters', () => {
 			it( 'consumes consumable values properly', () => {
 			it( 'consumes consumable values properly', () => {
 				// Add callbacks that will fire before default ones.
 				// Add callbacks that will fire before default ones.
 				// This should prevent default callbacks doing anything.
 				// This should prevent default callbacks doing anything.
-				dispatcher.on( 'selection', ( evt, data, consumable ) => {
-					expect( consumable.consume( data.selection, 'selection' ) ).to.be.true;
+				dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+					expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true;
 				}, { priority: 'high' } );
 				}, { priority: 'high' } );
 
 
-				dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => {
-					expect( consumable.consume( data.item, 'attribute:bold' ) ).to.be.true;
+				dispatcher.on( 'attribute:bold', ( evt, data, conversionApi ) => {
+					expect( conversionApi.consumable.consume( data.item, 'attribute:bold' ) ).to.be.true;
 				}, { priority: 'high' } );
 				}, { priority: 'high' } );
 
 
 				// Similar test case as above.
 				// Similar test case as above.
@@ -430,12 +435,14 @@ describe( 'downcast-selection-converters', () => {
 					{ bold: 'true' }
 					{ 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 );
 				expect( viewSelection.rangeCount ).to.equal( 1 );
 
 
@@ -451,15 +458,17 @@ describe( 'downcast-selection-converters', () => {
 					{ bold: 'true' }
 					{ 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 );
 				expect( viewSelection.rangeCount ).to.equal( 1 );
 
 
@@ -471,10 +480,11 @@ describe( 'downcast-selection-converters', () => {
 		describe( 'clearFakeSelection', () => {
 		describe( 'clearFakeSelection', () => {
 			it( 'should clear fake selection', () => {
 			it( 'should clear fake selection', () => {
 				dispatcher.on( 'selection', clearFakeSelection() );
 				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;
 				expect( viewSelection.isFake ).to.be.false;
 			} );
 			} );
 		} );
 		} );
@@ -492,16 +502,17 @@ describe( 'downcast-selection-converters', () => {
 			model.schema.extend( '$text', { allowIn: 'td' } );
 			model.schema.extend( '$text', { allowIn: 'td' } );
 
 
 			// "Universal" converter to convert table structure.
 			// "Universal" converter to convert table structure.
-			const tableConverter = insertElement( modelItem => new ViewContainerElement( modelItem.name ) );
+			const containerCreator = ( modelElement, viewWriter ) => viewWriter.createContainerElement( modelElement.name );
+			const tableConverter = insertElement( containerCreator );
 			dispatcher.on( 'insert:table', tableConverter );
 			dispatcher.on( 'insert:table', tableConverter );
 			dispatcher.on( 'insert:tr', tableConverter );
 			dispatcher.on( 'insert:tr', tableConverter );
 			dispatcher.on( 'insert:td', tableConverter );
 			dispatcher.on( 'insert:td', tableConverter );
 
 
 			// Special converter for table cells.
 			// Special converter for table cells.
-			dispatcher.on( 'selection', ( evt, data, consumable, conversionApi ) => {
+			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
 				const selection = data.selection;
 				const selection = data.selection;
 
 
-				if ( !consumable.test( selection, 'selection' ) || selection.isCollapsed ) {
+				if ( !conversionApi.consumable.test( selection, 'selection' ) || selection.isCollapsed ) {
 					return;
 					return;
 				}
 				}
 
 
@@ -509,10 +520,10 @@ describe( 'downcast-selection-converters', () => {
 					const node = range.start.nodeAfter;
 					const node = range.start.nodeAfter;
 
 
 					if ( node == range.end.nodeBefore && node instanceof ModelElement && node.name == 'td' ) {
 					if ( node == range.end.nodeBefore && node instanceof ModelElement && node.name == 'td' ) {
-						consumable.consume( selection, 'selection' );
+						conversionApi.consumable.consume( selection, 'selection' );
 
 
 						const viewNode = conversionApi.mapper.toViewElement( node );
 						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 );
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 
 
 		// Convert model to view.
 		// 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.
 		// Stringify view and check if it is same as expected.
 		expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<div>' + expectedView + '</div>' );
 		expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<div>' + expectedView + '</div>' );

+ 51 - 37
packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

@@ -10,15 +10,17 @@ import ModelElement from '../../src/model/element';
 import ModelRange from '../../src/model/range';
 import ModelRange from '../../src/model/range';
 import ModelPosition from '../../src/model/position';
 import ModelPosition from '../../src/model/position';
 
 
+import View from '../../src/view/view';
 import ViewContainerElement from '../../src/view/containerelement';
 import ViewContainerElement from '../../src/view/containerelement';
 
 
 describe( 'DowncastDispatcher', () => {
 describe( 'DowncastDispatcher', () => {
-	let dispatcher, doc, root, differStub, model;
+	let dispatcher, doc, root, differStub, model, view;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		model = new Model();
 		model = new Model();
+		view = new View();
 		doc = model.document;
 		doc = model.document;
-		dispatcher = new DowncastDispatcher( model );
+		dispatcher = new DowncastDispatcher();
 		root = doc.createRoot();
 		root = doc.createRoot();
 
 
 		differStub = {
 		differStub = {
@@ -31,7 +33,7 @@ describe( 'DowncastDispatcher', () => {
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'should create DowncastDispatcher with given api', () => {
 		it( 'should create DowncastDispatcher with given api', () => {
 			const apiObj = {};
 			const apiObj = {};
-			const dispatcher = new DowncastDispatcher( model, { apiObj } );
+			const dispatcher = new DowncastDispatcher( { apiObj } );
 
 
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 			expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
 		} );
 		} );
@@ -46,7 +48,9 @@ describe( 'DowncastDispatcher', () => {
 
 
 			differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
 			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.calledOnce ).to.be.true;
 			expect( dispatcher.convertInsert.firstCall.args[ 0 ].isEqual( range ) ).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' } ];
 			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;
 			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' }
 				{ 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;
 			expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true;
 		} );
 		} );
@@ -94,7 +102,9 @@ describe( 'DowncastDispatcher', () => {
 				{ type: 'insert', position, length: 3 },
 				{ type: 'insert', position, length: 3 },
 			];
 			];
 
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 
 			expect( dispatcher.convertInsert.calledTwice ).to.be.true;
 			expect( dispatcher.convertInsert.calledTwice ).to.be.true;
 			expect( dispatcher.convertRemove.calledOnce ).to.be.true;
 			expect( dispatcher.convertRemove.calledOnce ).to.be.true;
@@ -112,7 +122,9 @@ describe( 'DowncastDispatcher', () => {
 				{ name: 'bar', range: barRange }
 				{ name: 'bar', range: barRange }
 			];
 			];
 
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 
 			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
 			expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
@@ -129,7 +141,9 @@ describe( 'DowncastDispatcher', () => {
 				{ name: 'bar', range: barRange }
 				{ name: 'bar', range: barRange }
 			];
 			];
 
 
-			dispatcher.convertChanges( differStub );
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, writer );
+			} );
 
 
 			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
 			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
@@ -149,7 +163,7 @@ describe( 'DowncastDispatcher', () => {
 			const loggedEvents = [];
 			const loggedEvents = [];
 
 
 			// We will check everything connected with insert event:
 			// We will check everything connected with insert event:
-			dispatcher.on( 'insert', ( evt, data, consumable ) => {
+			dispatcher.on( 'insert', ( evt, data, conversionApi ) => {
 				// Check if the item is correct.
 				// Check if the item is correct.
 				const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
 				const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
 				// Check if the range is correct.
 				// Check if the range is correct.
@@ -160,11 +174,11 @@ describe( 'DowncastDispatcher', () => {
 				// Check if the event name is correct.
 				// Check if the event name is correct.
 				expect( evt.name ).to.equal( 'insert:' + ( data.item.name || '$text' ) );
 				expect( evt.name ).to.equal( 'insert:' + ( data.item.name || '$text' ) );
 				// Check if model consumable is correct.
 				// Check if model consumable is correct.
-				expect( consumable.consume( data.item, 'insert' ) ).to.be.true;
+				expect( conversionApi.consumable.consume( data.item, 'insert' ) ).to.be.true;
 			} );
 			} );
 
 
 			// Same here.
 			// Same here.
-			dispatcher.on( 'attribute', ( evt, data, consumable ) => {
+			dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
 				const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
 				const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
 				const key = data.attributeKey;
 				const key = data.attributeKey;
 				const value = data.attributeNewValue;
 				const value = data.attributeNewValue;
@@ -173,7 +187,7 @@ describe( 'DowncastDispatcher', () => {
 				loggedEvents.push( log );
 				loggedEvents.push( log );
 
 
 				expect( evt.name ).to.equal( 'attribute:' + key + ':' + ( data.item.name || '$text' ) );
 				expect( evt.name ).to.equal( 'attribute:' + key + ':' + ( data.item.name || '$text' ) );
-				expect( consumable.consume( data.item, 'attribute:' + key ) ).to.be.true;
+				expect( conversionApi.consumable.consume( data.item, 'attribute:' + key ) ).to.be.true;
 			} );
 			} );
 
 
 			dispatcher.convertInsert( range );
 			dispatcher.convertInsert( range );
@@ -200,9 +214,9 @@ describe( 'DowncastDispatcher', () => {
 
 
 			sinon.spy( dispatcher, 'fire' );
 			sinon.spy( dispatcher, 'fire' );
 
 
-			dispatcher.on( 'insert:image', ( evt, data, consumable ) => {
-				consumable.consume( data.item.getChild( 0 ), 'insert' );
-				consumable.consume( data.item, 'attribute:bold' );
+			dispatcher.on( 'insert:image', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item.getChild( 0 ), 'insert' );
+				conversionApi.consumable.consume( data.item, 'attribute:bold' );
 			} );
 			} );
 
 
 			const range = ModelRange.createIn( root );
 			const range = ModelRange.createIn( root );
@@ -250,7 +264,7 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should fire selection event', () => {
 		it( 'should fire selection event', () => {
 			sinon.spy( dispatcher, 'fire' );
 			sinon.spy( dispatcher, 'fire' );
 
 
-			dispatcher.convertSelection( doc.selection, [] );
+			dispatcher.convertSelection( doc.selection, model.markers, [] );
 
 
 			expect( dispatcher.fire.calledWith(
 			expect( dispatcher.fire.calledWith(
 				'selection',
 				'selection',
@@ -264,13 +278,13 @@ describe( 'DowncastDispatcher', () => {
 				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 			} );
 
 
-			dispatcher.on( 'selection', ( evt, data, consumable ) => {
-				expect( consumable.test( data.selection, 'selection' ) ).to.be.true;
-				expect( consumable.test( data.selection, 'attribute:bold' ) ).to.be.true;
-				expect( consumable.test( data.selection, 'attribute:italic' ) ).to.be.null;
+			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+				expect( conversionApi.consumable.test( data.selection, 'selection' ) ).to.be.true;
+				expect( conversionApi.consumable.test( data.selection, 'attribute:bold' ) ).to.be.true;
+				expect( conversionApi.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', () => {
 		it( 'should not fire attributes events for non-collapsed selection', () => {
@@ -281,7 +295,7 @@ describe( 'DowncastDispatcher', () => {
 
 
 			sinon.spy( dispatcher, 'fire' );
 			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:bold' ) ).to.be.false;
 			expect( dispatcher.fire.calledWith( 'attribute:italic' ) ).to.be.false;
 			expect( dispatcher.fire.calledWith( 'attribute:italic' ) ).to.be.false;
@@ -300,7 +314,7 @@ describe( 'DowncastDispatcher', () => {
 
 
 			sinon.spy( dispatcher, 'fire' );
 			sinon.spy( dispatcher, 'fire' );
 
 
-			dispatcher.convertSelection( doc.selection, [] );
+			dispatcher.convertSelection( doc.selection, model.markers, [] );
 
 
 			expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.true;
 		} );
 		} );
@@ -317,13 +331,13 @@ describe( 'DowncastDispatcher', () => {
 				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
 			} );
 			} );
 
 
-			dispatcher.on( 'selection', ( evt, data, consumable ) => {
-				consumable.consume( data.selection, 'attribute:bold' );
+			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.selection, 'attribute:bold' );
 			} );
 			} );
 
 
 			sinon.spy( dispatcher, 'fire' );
 			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:bold' ) ).to.be.false;
 		} );
 		} );
@@ -339,7 +353,7 @@ describe( 'DowncastDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 			sinon.spy( dispatcher, 'fire' );
 
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 			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;
 			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.true;
 		} );
 		} );
@@ -352,7 +366,7 @@ describe( 'DowncastDispatcher', () => {
 			sinon.spy( dispatcher, 'fire' );
 			sinon.spy( dispatcher, 'fire' );
 
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 			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;
 			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
 		} );
 		} );
@@ -371,8 +385,8 @@ describe( 'DowncastDispatcher', () => {
 			const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
 			const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
 
 
 			// Create custom highlight handler mock.
 			// Create custom highlight handler mock.
-			viewFigure.setCustomProperty( 'addHighlight', () => { } );
-			viewFigure.setCustomProperty( 'removeHighlight', () => { } );
+			viewFigure._setCustomProperty( 'addHighlight', () => { } );
+			viewFigure._setCustomProperty( 'removeHighlight', () => { } );
 
 
 			// Create mapper mock.
 			// Create mapper mock.
 			dispatcher.conversionApi.mapper = {
 			dispatcher.conversionApi.mapper = {
@@ -393,7 +407,7 @@ describe( 'DowncastDispatcher', () => {
 
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 			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;
 			expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
 		} );
 		} );
@@ -409,12 +423,12 @@ describe( 'DowncastDispatcher', () => {
 
 
 			sinon.spy( dispatcher, 'fire' );
 			sinon.spy( dispatcher, 'fire' );
 
 
-			dispatcher.on( 'addMarker:foo', ( evt, data, consumable ) => {
-				consumable.consume( data.item, 'addMarker:bar' );
+			dispatcher.on( 'addMarker:foo', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'addMarker:bar' );
 			} );
 			} );
 
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
 			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:foo' ) ).to.be.true;
 			expect( dispatcher.fire.calledWith( 'addMarker:bar' ) ).to.be.false;
 			expect( dispatcher.fire.calledWith( 'addMarker:bar' ) ).to.be.false;
@@ -464,10 +478,10 @@ describe( 'DowncastDispatcher', () => {
 
 
 			const items = [];
 			const items = [];
 
 
-			dispatcher.on( 'addMarker:name', ( evt, data, consumable ) => {
+			dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => {
 				expect( data.markerName ).to.equal( 'name' );
 				expect( data.markerName ).to.equal( 'name' );
 				expect( data.markerRange.isEqual( range ) ).to.be.true;
 				expect( data.markerRange.isEqual( range ) ).to.be.true;
-				expect( consumable.test( data.item, 'addMarker:name' ) );
+				expect( conversionApi.consumable.test( data.item, 'addMarker:name' ) );
 
 
 				items.push( data.item );
 				items.push( data.item );
 			} );
 			} );

+ 0 - 537
packages/ckeditor5-engine/tests/conversion/two-way-converters.js

@@ -1,537 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import {
-	elementToElement, attributeToElement, attributeToAttribute
-} from '../../src/conversion/two-way-converters';
-
-import Conversion from '../../src/conversion/conversion';
-import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
-
-import { convertText, convertToModelFragment } from '../../src/conversion/upcast-converters';
-
-import EditingController from '../../src/controller/editingcontroller';
-
-import Model from '../../src/model/model';
-import ModelRange from '../../src/model/range';
-
-import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view';
-import { stringify as modelStringify } from '../../src/dev-utils/model';
-
-describe( 'two-way-converters', () => {
-	let viewDispatcher, model, schema, conversion, modelRoot, viewRoot;
-
-	beforeEach( () => {
-		model = new Model();
-		const controller = new EditingController( model );
-
-		const modelDoc = model.document;
-		modelRoot = modelDoc.createRoot();
-
-		viewRoot = controller.view.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';
-
-		schema = model.schema;
-
-		schema.extend( '$text', {
-			allowAttributes: [ 'bold' ]
-		} );
-
-		schema.register( 'paragraph', {
-			inheritAllFrom: '$block'
-		} );
-
-		viewDispatcher = new UpcastDispatcher( model, { schema } );
-		viewDispatcher.on( 'text', convertText() );
-		viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-		viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
-
-		conversion = new Conversion();
-		conversion.register( 'upcast', [ viewDispatcher ] );
-		conversion.register( 'downcast', [ controller.downcastDispatcher ] );
-	} );
-
-	describe( 'elementToElement', () => {
-		it( 'config.view is a string', () => {
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-
-			test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			schema.register( 'fancyParagraph', {
-				inheritAllFrom: 'paragraph'
-			} );
-
-			elementToElement( conversion, {
-				model: 'fancyParagraph',
-				view: {
-					name: 'p',
-					class: 'fancy'
-				}
-			} );
-
-			test( '<p class="fancy">Foo</p>', '<fancyParagraph>Foo</fancyParagraph>' );
-		} );
-
-		it( 'config.view is an object with upcastAlso defined', () => {
-			elementToElement( conversion, {
-				model: 'paragraph',
-				view: 'p',
-				upcastAlso: [
-					'div',
-					{
-						// Match any name.
-						name: /./,
-						style: {
-							display: 'block'
-						}
-					}
-				]
-			} );
-
-			test( '<p>Foo</p>', '<paragraph>Foo</paragraph>' );
-			test( '<div>Foo</div>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
-			test( '<span style="display:block">Foo</span>', '<paragraph>Foo</paragraph>', '<p>Foo</p>' );
-		} );
-
-		it( 'upcastAlso given as a function', () => {
-			schema.register( 'heading', {
-				inheritAllFrom: '$block'
-			} );
-
-			elementToElement( conversion, {
-				model: 'heading',
-				view: 'h2',
-				upcastAlso: viewElement => {
-					const fontSize = viewElement.getStyle( 'font-size' );
-
-					if ( !fontSize ) {
-						return null;
-					}
-
-					const match = fontSize.match( /(\d+)\s*px/ );
-
-					if ( !match ) {
-						return null;
-					}
-
-					const size = Number( match[ 1 ] );
-
-					if ( size >= 26 ) {
-						return { name: true, style: [ 'font-size' ] };
-					}
-
-					return null;
-				}
-			} );
-
-			elementToElement( conversion, {
-				model: 'paragraph',
-				view: 'p'
-			} );
-
-			test( '<p></p>', '<paragraph></paragraph>' );
-			test( '<p style="font-size:20px"></p>', '<paragraph></paragraph>', '<p></p>' );
-
-			test( '<h2></h2>', '<heading></heading>' );
-			test( '<p style="font-size:26px"></p>', '<heading></heading>', '<h2></h2>' );
-		} );
-	} );
-
-	describe( 'attributeToElement', () => {
-		beforeEach( () => {
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-		} );
-
-		it( 'config.view is a string', () => {
-			attributeToElement( conversion, 'bold', { view: 'strong' } );
-
-			test( '<p><strong>Foo</strong> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			attributeToElement( conversion, 'bold', {
-				view: {
-					name: 'span',
-					class: 'bold'
-				}
-			} );
-
-			test( '<p><span class="bold">Foo</span> bar</p>', '<paragraph><$text bold="true">Foo</$text> bar</paragraph>' );
-		} );
-
-		it( 'config.view is an object with upcastAlso defined', () => {
-			attributeToElement( conversion, 'bold', {
-				view: 'strong',
-				upcastAlso: [
-					'b',
-					{
-						name: 'span',
-						class: 'bold'
-					},
-					{
-						name: 'span',
-						style: {
-							'font-weight': 'bold'
-						}
-					},
-					viewElement => {
-						const fontWeight = viewElement.getStyle( 'font-weight' );
-
-						if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
-							return {
-								name: true,
-								style: [ 'font-weight' ]
-							};
-						}
-					}
-				]
-			} );
-
-			test(
-				'<p><strong>Foo</strong></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>'
-			);
-
-			test(
-				'<p><b>Foo</b></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-
-			test(
-				'<p><span class="bold">Foo</span></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-
-			test(
-				'<p><span style="font-weight: bold;">Foo</span></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-
-			test(
-				'<p><span style="font-weight: 500;">Foo</span></p>',
-				'<paragraph>Foo</paragraph>',
-				'<p>Foo</p>'
-			);
-
-			test(
-				'<p><span style="font-weight: 600;">Foo</span></p>',
-				'<paragraph><$text bold="true">Foo</$text></paragraph>',
-				'<p><strong>Foo</strong></p>'
-			);
-		} );
-
-		it( 'config.model is a string', () => {
-			schema.extend( '$text', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			attributeToElement( conversion, 'styled', {
-				model: 'dark',
-				view: {
-					name: 'span',
-					class: [ 'styled', 'styled-dark' ]
-				}
-			} );
-
-			test( '<p><span class="styled styled-dark">Foo</span> bar</p>', '<paragraph><$text styled="dark">Foo</$text> bar</paragraph>' );
-		} );
-
-		it( 'config is an array', () => {
-			schema.extend( '$text', {
-				allowAttributes: [ 'fontSize' ]
-			} );
-
-			attributeToElement( conversion, 'fontSize', [
-				{
-					model: 'big',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '1.2em'
-						}
-					}
-				},
-				{
-					model: 'small',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '0.8em'
-						}
-					}
-				}
-			] );
-
-			test(
-				'<p><span style="font-size:1.2em">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>'
-			);
-
-			test(
-				'<p><span style="font-size:0.8em">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
-			);
-		} );
-
-		it( 'config is an array with upcastAlso defined', () => {
-			schema.extend( '$text', {
-				allowAttributes: [ 'fontSize' ]
-			} );
-
-			attributeToElement( conversion, 'fontSize', [
-				{
-					model: 'big',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '1.2em'
-						}
-					},
-					upcastAlso: viewElement => {
-						const fontSize = viewElement.getStyle( 'font-size' );
-
-						if ( !fontSize ) {
-							return null;
-						}
-
-						const match = fontSize.match( /(\d+)\s*px/ );
-
-						if ( !match ) {
-							return null;
-						}
-
-						const size = Number( match[ 1 ] );
-
-						if ( viewElement.is( 'span' ) && size > 10 ) {
-							return { name: true, style: [ 'font-size' ] };
-						}
-
-						return null;
-					}
-				},
-				{
-					model: 'small',
-					view: {
-						name: 'span',
-						style: {
-							'font-size': '0.8em'
-						}
-					},
-					upcastAlso: viewElement => {
-						const fontSize = viewElement.getStyle( 'font-size' );
-
-						if ( !fontSize ) {
-							return null;
-						}
-
-						const match = fontSize.match( /(\d+)\s*px/ );
-
-						if ( !match ) {
-							return null;
-						}
-
-						const size = Number( match[ 1 ] );
-
-						if ( viewElement.is( 'span' ) && size < 10 ) {
-							return { name: true, style: [ 'font-size' ] };
-						}
-
-						return null;
-					}
-				}
-			] );
-
-			test(
-				'<p><span style="font-size:1.2em">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>'
-			);
-
-			test(
-				'<p><span style="font-size:12px">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="big">Foo</$text> bar</paragraph>',
-				'<p><span style="font-size:1.2em">Foo</span> bar</p>'
-			);
-
-			test(
-				'<p><span style="font-size:0.8em">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>'
-			);
-
-			test(
-				'<p><span style="font-size:8px">Foo</span> bar</p>',
-				'<paragraph><$text fontSize="small">Foo</$text> bar</paragraph>',
-				'<p><span style="font-size:0.8em">Foo</span> bar</p>'
-			);
-
-			test(
-				'<p><span style="font-size:10px">Foo</span> bar</p>',
-				'<paragraph>Foo bar</paragraph>',
-				'<p>Foo bar</p>'
-			);
-		} );
-	} );
-
-	describe( 'attributeToAttribute', () => {
-		beforeEach( () => {
-			elementToElement( conversion, { model: 'image', view: 'img' } );
-
-			schema.register( 'image', {
-				inheritAllFrom: '$block',
-			} );
-		} );
-
-		it( 'config is not set', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'src' ]
-			} );
-
-			attributeToAttribute( conversion, 'src' );
-
-			test( '<img src="foo.jpg"></img>', '<image src="foo.jpg"></image>' );
-		} );
-
-		it( 'config.view is a string', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'source' ]
-			} );
-
-			attributeToAttribute( conversion, 'source', { view: 'src' } );
-
-			test( '<img src="foo.jpg"></img>', '<image source="foo.jpg"></image>' );
-		} );
-
-		it( 'config.view is an object', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'aside' ]
-			} );
-
-			attributeToAttribute( conversion, 'aside', {
-				model: true,
-				view: {
-					name: 'img',
-					key: 'class',
-					value: 'aside half-size'
-				}
-			} );
-
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-
-			test( '<img class="aside half-size"></img>', '<image aside="true"></image>' );
-			test( '<p class="aside half-size"></p>', '<paragraph></paragraph>', '<p></p>' );
-		} );
-
-		it( 'config is an array', () => {
-			schema.extend( 'image', {
-				allowAttributes: [ 'styled' ]
-			} );
-
-			attributeToAttribute( conversion, 'styled', [
-				{
-					model: 'dark',
-					view: {
-						key: 'class',
-						value: 'styled styled-dark'
-					}
-				},
-				{
-					model: 'light',
-					view: {
-						key: 'class',
-						value: 'styled styled-light'
-					}
-				}
-			] );
-
-			test( '<img class="styled styled-dark"></img>', '<image styled="dark"></image>' );
-			test( '<img class="styled styled-light"></img>', '<image styled="light"></image>' );
-		} );
-
-		it( 'config is an array with upcastAlso defined', () => {
-			elementToElement( conversion, { model: 'paragraph', view: 'p' } );
-
-			schema.extend( 'paragraph', {
-				allowAttributes: [ 'align' ]
-			} );
-
-			attributeToAttribute( conversion, 'align', [
-				{
-					model: 'right',
-					view: {
-						key: 'class',
-						value: 'align-right'
-					},
-					upcastAlso: viewElement => {
-						if ( viewElement.getStyle( 'text-align' ) == 'right' ) {
-							return {
-								style: [ 'text-align' ]
-							};
-						}
-
-						return null;
-					}
-				},
-				{
-					model: 'center',
-					view: {
-						key: 'class',
-						value: 'align-center'
-					},
-					upcastAlso: {
-						style: {
-							'text-align': 'center'
-						}
-					}
-				}
-			] );
-
-			test(
-				'<p class="align-right">Foo</p>',
-				'<paragraph align="right">Foo</paragraph>'
-			);
-
-			test(
-				'<p style="text-align:right">Foo</p>',
-				'<paragraph align="right">Foo</paragraph>',
-				'<p class="align-right">Foo</p>'
-			);
-
-			test(
-				'<p class="align-center">Foo</p>',
-				'<paragraph align="center">Foo</paragraph>'
-			);
-
-			test(
-				'<p style="text-align:center">Foo</p>',
-				'<paragraph align="center">Foo</paragraph>',
-				'<p class="align-center">Foo</p>'
-			);
-		} );
-	} );
-
-	function test( input, expectedModel, expectedView = null ) {
-		loadData( input );
-
-		expect( modelStringify( model.document.getRoot() ) ).to.equal( expectedModel );
-		expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( expectedView || input );
-	}
-
-	function loadData( input ) {
-		const parsedView = viewParse( input );
-
-		const convertedModel = viewDispatcher.convert( parsedView );
-
-		model.change( writer => {
-			writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );
-			writer.insert( convertedModel, modelRoot, 0 );
-		} );
-	}
-} );

+ 33 - 51
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -27,7 +27,7 @@ import {
 import { stringify } from '../../src/dev-utils/model';
 import { stringify } from '../../src/dev-utils/model';
 
 
 describe( 'upcast-helpers', () => {
 describe( 'upcast-helpers', () => {
-	let dispatcher, model, schema, conversion;
+	let upcastDispatcher, model, schema, conversion;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		model = new Model();
 		model = new Model();
@@ -50,13 +50,13 @@ describe( 'upcast-helpers', () => {
 			allowAttributes: [ 'bold' ]
 			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 = new Conversion();
-		conversion.register( 'upcast', [ dispatcher ] );
+		conversion.register( 'upcast', [ upcastDispatcher ] );
 	} );
 	} );
 
 
 	describe( 'upcastElementToElement', () => {
 	describe( 'upcastElementToElement', () => {
@@ -74,7 +74,7 @@ describe( 'upcast-helpers', () => {
 			} );
 			} );
 
 
 			const helperA = upcastElementToElement( { view: 'p', model: 'p' } );
 			const helperA = upcastElementToElement( { view: 'p', model: 'p' } );
-			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph' }, 'high' );
+			const helperB = upcastElementToElement( { view: 'p', model: 'paragraph', priority: 'high' } );
 
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
 
@@ -86,37 +86,17 @@ describe( 'upcast-helpers', () => {
 				inheritAllFrom: '$block'
 				inheritAllFrom: '$block'
 			} );
 			} );
 
 
-			const helperParagraph = upcastElementToElement( { view: 'p', model: 'paragraph' } );
 			const helperFancy = upcastElementToElement( {
 			const helperFancy = upcastElementToElement( {
 				view: {
 				view: {
 					name: 'p',
 					name: 'p',
 					class: 'fancy'
 					class: 'fancy'
 				},
 				},
-				model: 'fancyParagraph'
-			}, 'high' );
-
-			conversion.for( 'upcast' ).add( helperParagraph ).add( helperFancy );
-
-			expectResult( new ViewContainerElement( 'p' ), '<paragraph></paragraph>' );
-			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
-		} );
-
-		it( 'config.model is element instance', () => {
-			schema.extend( 'paragraph', {
-				allowAttributes: [ 'fancy' ]
+				model: 'fancyParagraph',
 			} );
 			} );
 
 
-			const helper = upcastElementToElement( {
-				view: {
-					name: 'p',
-					class: 'fancy'
-				},
-				model: new ModelElement( 'paragraph', { fancy: true } )
-			} );
+			conversion.for( 'upcast' ).add( helperFancy );
 
 
-			conversion.for( 'upcast' ).add( helper );
-
-			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<paragraph fancy="true"></paragraph>' );
+			expectResult( new ViewContainerElement( 'p', { class: 'fancy' } ), '<fancyParagraph></fancyParagraph>' );
 		} );
 		} );
 
 
 		it( 'config.model is a function', () => {
 		it( 'config.model is a function', () => {
@@ -130,7 +110,9 @@ describe( 'upcast-helpers', () => {
 					name: 'p',
 					name: 'p',
 					class: 'heading'
 					class: 'heading'
 				},
 				},
-				model: viewElement => new ModelElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } )
+				model: ( viewElement, modelWriter ) => {
+					return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
+				}
 			} );
 			} );
 
 
 			conversion.for( 'upcast' ).add( helper );
 			conversion.for( 'upcast' ).add( helper );
@@ -176,7 +158,7 @@ describe( 'upcast-helpers', () => {
 
 
 		it( 'should not do anything if returned model element is null', () => {
 		it( 'should not do anything if returned model element is null', () => {
 			const helperA = upcastElementToElement( { view: 'p', model: 'paragraph' } );
 			const helperA = upcastElementToElement( { view: 'p', model: 'paragraph' } );
-			const helperB = upcastElementToElement( { view: 'p', model: () => null }, 'high' );
+			const helperB = upcastElementToElement( { view: 'p', model: () => null, priority: 'high' } );
 
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
 
@@ -198,7 +180,7 @@ describe( 'upcast-helpers', () => {
 
 
 		it( 'can be overwritten using priority', () => {
 		it( 'can be overwritten using priority', () => {
 			const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } );
 			const helperA = upcastElementToAttribute( { view: 'strong', model: 'strong' } );
-			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold' }, 'high' );
+			const helperB = upcastElementToAttribute( { view: 'strong', model: 'bold', priority: 'high' } );
 
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
 
@@ -314,8 +296,9 @@ describe( 'upcast-helpers', () => {
 				model: {
 				model: {
 					key: 'bold',
 					key: 'bold',
 					value: () => null
 					value: () => null
-				}
-			}, 'high' );
+				},
+				priority: 'high'
+			} );
 
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
 
@@ -371,7 +354,7 @@ describe( 'upcast-helpers', () => {
 			} );
 			} );
 
 
 			const helperA = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } );
 			const helperA = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'src' } );
-			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' }, 'normal' );
+			const helperB = upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', priority: 'normal' } );
 
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
 
@@ -536,7 +519,7 @@ describe( 'upcast-helpers', () => {
 
 
 		it( 'can be overwritten using priority', () => {
 		it( 'can be overwritten using priority', () => {
 			const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } );
 			const helperA = upcastElementToMarker( { view: 'marker-search', model: 'search-result' } );
-			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search' }, 'high' );
+			const helperB = upcastElementToMarker( { view: 'marker-search', model: 'search', priority: 'high' } );
 
 
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 			conversion.for( 'upcast' ).add( helperA ).add( helperB );
 
 
@@ -600,18 +583,18 @@ describe( 'upcast-helpers', () => {
 	} );
 	} );
 
 
 	function expectResult( viewToConvert, modelString, marker ) {
 	function expectResult( viewToConvert, modelString, marker ) {
-		const model = dispatcher.convert( viewToConvert );
+		const conversionResult = model.change( writer => upcastDispatcher.convert( viewToConvert, writer ) );
 
 
 		if ( marker ) {
 		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.start.path ).to.deep.equal( marker.start );
 			expect( convertedMarker.end.path ).to.deep.equal( marker.end );
 			expect( convertedMarker.end.path ).to.deep.equal( marker.end );
 		}
 		}
 
 
-		expect( stringify( model ) ).to.equal( modelString );
+		expect( stringify( conversionResult ) ).to.equal( modelString );
 	}
 	}
 } );
 } );
 
 
@@ -627,7 +610,7 @@ describe( 'upcast-converters', () => {
 
 
 		context = [ '$root' ];
 		context = [ '$root' ];
 
 
-		dispatcher = new UpcastDispatcher( model, { schema } );
+		dispatcher = new UpcastDispatcher( { schema } );
 	} );
 	} );
 
 
 	describe( 'convertText()', () => {
 	describe( 'convertText()', () => {
@@ -636,7 +619,7 @@ describe( 'upcast-converters', () => {
 
 
 			dispatcher.on( 'text', convertText() );
 			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 ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -658,7 +641,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 ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -674,13 +657,12 @@ describe( 'upcast-converters', () => {
 
 
 			const viewText = new ViewText( 'foobar' );
 			const viewText = new ViewText( 'foobar' );
 			dispatcher.on( 'text', convertText() );
 			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 ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 0 );
 			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 ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.childCount ).to.equal( 1 );
 			expect( conversionResult.childCount ).to.equal( 1 );
@@ -693,7 +675,7 @@ describe( 'upcast-converters', () => {
 
 
 			dispatcher.on( 'text', convertText() );
 			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 ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText );
@@ -714,7 +696,7 @@ describe( 'upcast-converters', () => {
 			dispatcher.on( 'element', convertToModelFragment() );
 			dispatcher.on( 'element', convertToModelFragment() );
 			dispatcher.on( 'documentFragment', 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 ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.maxOffset ).to.equal( 6 );
 			expect( conversionResult.maxOffset ).to.equal( 6 );
@@ -741,7 +723,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 ).to.be.instanceof( ModelDocumentFragment );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );
 			expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement );
@@ -775,7 +757,7 @@ describe( 'upcast-converters', () => {
 				spy();
 				spy();
 			} );
 			} );
 
 
-			dispatcher.convert( view );
+			model.change( writer => dispatcher.convert( view, writer ) );
 
 
 			sinon.assert.calledTwice( spy );
 			sinon.assert.calledTwice( spy );
 		} );
 		} );

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

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

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

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

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

@@ -474,8 +474,8 @@ describe( 'ViewConsumable', () => {
 		} );
 		} );
 
 
 		it( 'should add all attributes', () => {
 		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 );
 			const consumables = ViewConsumable.consumablesFromElement( el );
 			expect( consumables.attribute.length ).to.equal( 2 );
 			expect( consumables.attribute.length ).to.equal( 2 );
@@ -487,7 +487,7 @@ describe( 'ViewConsumable', () => {
 		} );
 		} );
 
 
 		it( 'should add all classes', () => {
 		it( 'should add all classes', () => {
-			el.addClass( 'foo', 'bar', 'baz' );
+			el._addClass( [ 'foo', 'bar', 'baz' ] );
 
 
 			const consumables = ViewConsumable.consumablesFromElement( el );
 			const consumables = ViewConsumable.consumablesFromElement( el );
 			expect( consumables.class.length ).to.equal( 3 );
 			expect( consumables.class.length ).to.equal( 3 );
@@ -500,7 +500,7 @@ describe( 'ViewConsumable', () => {
 		} );
 		} );
 
 
 		it( 'should add all styles', () => {
 		it( 'should add all styles', () => {
-			el.setStyle( {
+			el._setStyle( {
 				color: 'red',
 				color: 'red',
 				position: 'absolute'
 				position: 'absolute'
 			} );
 			} );

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

@@ -788,7 +788,8 @@ describe( 'debug tools', () => {
 			const model = editor.model;
 			const model = editor.model;
 			const modelDoc = model.document;
 			const modelDoc = model.document;
 			const modelRoot = modelDoc.getRoot();
 			const modelRoot = modelDoc.getRoot();
-			const viewDoc = editor.editing.view;
+			const view = editor.editing.view;
+			const viewDoc = view.document;
 
 
 			model.change( () => {
 			model.change( () => {
 				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
 				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 Text from '../../src/view/text';
 import Selection from '../../src/view/selection';
 import Selection from '../../src/view/selection';
 import Range from '../../src/view/range';
 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 XmlDataProcessor from '../../src/dataprocessor/xmldataprocessor';
 import createViewRoot from '../view/_utils/createroot';
 import createViewRoot from '../view/_utils/createroot';
 
 
@@ -36,12 +36,13 @@ describe( 'view test utils', () => {
 			it( 'should use stringify method', () => {
 			it( 'should use stringify method', () => {
 				const element = document.createElement( 'div' );
 				const element = document.createElement( 'div' );
 				const stringifySpy = sandbox.spy( getData, '_stringify' );
 				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 options = { showType: false, showPriority: false, withoutSelection: true };
 				const root = createAttachedRoot( viewDocument, element );
 				const root = createAttachedRoot( viewDocument, element );
 				root.appendChildren( new Element( 'p' ) );
 				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 );
 				sinon.assert.calledOnce( stringifySpy );
 				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
 				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
 				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( null );
 				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( 'showPriority' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
 
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 			} );
 
 
 			it( 'should use stringify method with selection', () => {
 			it( 'should use stringify method with selection', () => {
 				const element = document.createElement( 'div' );
 				const element = document.createElement( 'div' );
 				const stringifySpy = sandbox.spy( getData, '_stringify' );
 				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 options = { showType: false, showPriority: false };
 				const root = createAttachedRoot( viewDocument, element );
 				const root = createAttachedRoot( viewDocument, element );
 				root.appendChildren( new Element( 'p' ) );
 				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 );
 				sinon.assert.calledOnce( stringifySpy );
 				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
 				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
 				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( viewDocument.selection );
 				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( 'showPriority' ).that.equals( false );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
 				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
 
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 			} );
 
 
 			it( 'should throw an error when passing invalid document', () => {
 			it( 'should throw an error when passing invalid document', () => {
 				expect( () => {
 				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', () => {
 		describe( 'setData', () => {
 			it( 'should use parse method', () => {
 			it( 'should use parse method', () => {
-				const viewDocument = new Document();
+				const view = new View();
+				const viewDocument = view.document;
 				const data = 'foobar<b>baz</b>';
 				const data = 'foobar<b>baz</b>';
 				const parseSpy = sandbox.spy( setData, '_parse' );
 				const parseSpy = sandbox.spy( setData, '_parse' );
 
 
 				createAttachedRoot( viewDocument, document.createElement( 'div' ) );
 				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 );
 				sinon.assert.calledOnce( parseSpy );
 				const args = parseSpy.firstCall.args;
 				const args = parseSpy.firstCall.args;
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
 
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 			} );
 
 
 			it( 'should use parse method with selection', () => {
 			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 data = '[<b>baz</b>]';
 				const parseSpy = sandbox.spy( setData, '_parse' );
 				const parseSpy = sandbox.spy( setData, '_parse' );
 
 
 				createAttachedRoot( viewDocument, document.createElement( 'div' ) );
 				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;
 				const args = parseSpy.firstCall.args;
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 0 ] ).to.equal( data );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ] ).to.be.an( 'object' );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
 				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
 
 
-				viewDocument.destroy();
+				view.destroy();
 			} );
 			} );
 
 
 			it( 'should throw an error when passing invalid document', () => {
 			it( 'should throw an error when passing invalid document', () => {
 				expect( () => {
 				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', () => {
 		it( 'should parse element priority', () => {
 			const parsed1 = parse( '<b view-priority="12"></b>' );
 			const parsed1 = parse( '<b view-priority="12"></b>' );
 			const attribute1 = new AttributeElement( 'b' );
 			const attribute1 = new AttributeElement( 'b' );
-			attribute1.priority = 12;
+			attribute1._priority = 12;
 			const parsed2 = parse( '<attribute:b view-priority="44"></attribute:b>' );
 			const parsed2 = parse( '<attribute:b view-priority="44"></attribute:b>' );
 			const attribute2 = new AttributeElement( 'b' );
 			const attribute2 = new AttributeElement( 'b' );
-			attribute2.priority = 44;
+			attribute2._priority = 44;
 
 
 			parsed1.isSimilar( attribute1 );
 			parsed1.isSimilar( attribute1 );
 			expect( parsed1.isSimilar( attribute1 ) ).to.be.true;
 			expect( parsed1.isSimilar( attribute1 ) ).to.be.true;

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

@@ -6,8 +6,7 @@
 /* global console, window, document */
 /* global console, window, document */
 
 
 import ModelRange from '../../src/model/range';
 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 {
 import {
 	upcastElementToElement,
 	upcastElementToElement,
@@ -40,7 +39,7 @@ class FancyWidget extends Plugin {
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const schema = editor.model.schema;
 		const schema = editor.model.schema;
-		const data = editor.data;
+		const conversion = editor.conversion;
 
 
 		// Configure schema.
 		// Configure schema.
 		schema.register( 'fancywidget', {
 		schema.register( 'fancywidget', {
@@ -48,19 +47,22 @@ class FancyWidget extends Plugin {
 		} );
 		} );
 		schema.extend( 'fancywidget', { allowIn: '$root' } );
 		schema.extend( 'fancywidget', { allowIn: '$root' } );
 
 
-		downcastElementToElement( {
+		conversion.for( 'editingDowncast' ).add( downcastElementToElement( {
 			model: 'fancywidget',
 			model: 'fancywidget',
-			view: () => {
-				const widgetElement = new ViewContainerElement( 'figure', { class: 'fancy-widget' }, new ViewText( 'widget' ) );
+			view: ( modelItem, 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 => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;
 
 
-		downcastMarkerToHighlight( {
+		editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( {
 			model: 'marker',
 			model: 'marker',
 			view: data => ( {
 			view: data => ( {
 				class: 'highlight-' + data.markerName.split( ':' )[ 1 ]
 				class: 'highlight-' + data.markerName.split( ':' )[ 1 ]
 			} )
 			} )
-		} );
+		} ) );
 
 
 		document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => {
 		document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => {
 			addMarker( editor, 'yellow' );
 			addMarker( editor, 'yellow' );

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

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

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

@@ -13,7 +13,6 @@ import {
 	downcastElementToElement
 	downcastElementToElement
 } from '../../src/conversion/downcast-converters';
 } from '../../src/conversion/downcast-converters';
 
 
-import ViewEditableElement from '../../src/view/editableelement';
 import { getData } from '../../src/dev-utils/model';
 import { getData } from '../../src/dev-utils/model';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 
@@ -30,7 +29,6 @@ class NestedEditable extends Plugin {
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
 		const editing = editor.editing;
 		const editing = editor.editing;
-		const viewDocument = editing.view;
 		const schema = editor.model.schema;
 		const schema = editor.model.schema;
 
 
 		schema.register( 'figure', {
 		schema.register( 'figure', {
@@ -60,15 +58,15 @@ class NestedEditable extends Plugin {
 
 
 		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
 		editor.conversion.for( 'downcast' ).add( downcastElementToElement( {
 			model: 'figcaption',
 			model: 'figcaption',
-			view: () => {
-				const element = new ViewEditableElement( 'figcaption', { contenteditable: 'true' } );
-				element.document = viewDocument;
+			view: ( modelItem, conversionApi ) => {
+				const viewWriter = conversionApi.writer;
+				const element = viewWriter.createEditableElement( 'figcaption', { contenteditable: 'true' } );
 
 
 				element.on( 'change:isFocused', ( evt, property, is ) => {
 				element.on( 'change:isFocused', ( evt, property, is ) => {
 					if ( is ) {
 					if ( is ) {
-						element.addClass( 'focused' );
+						editing.view.change( writer => writer.addClass( 'focused', element ) );
 					} else {
 					} 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' ]
 		toolbar: [ 'headings', '|', 'undo', 'redo' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
-		const viewDoc = editor.editing.view;
+		const view = editor.editing.view;
+		const viewDoc = view.document;
 		const header = viewDoc.getRoot().getChild( 0 );
 		const header = viewDoc.getRoot().getChild( 0 );
 		const paragraph = viewDoc.getRoot().getChild( 1 );
 		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 => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

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

@@ -18,7 +18,7 @@ ClassicEditor
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;
 
 
-		editor.editing.view.on( 'selectionChange', () => {
+		editor.editing.view.document.on( 'selectionChange', () => {
 			editor.model.change( () => {
 			editor.model.change( () => {
 			} );
 			} );
 			console.log( 'selectionChange', ( new Date() ).getTime() );
 			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 { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 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 { downcastElementToElement } from '../../../../src/conversion/downcast-converters';
 import { setData } from '../../../../src/dev-utils/model';
 import { setData } from '../../../../src/dev-utils/model';
-import ViewEditable from '../../../../src/view/editableelement';
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
@@ -45,16 +43,19 @@ ClassicEditor
 		editor.conversion.for( 'downcast' )
 		editor.conversion.for( 'downcast' )
 			.add( downcastElementToElement( {
 			.add( downcastElementToElement( {
 				model: 'widget',
 				model: 'widget',
-				view: () => {
-					const b = new AttributeContainer( 'b' );
-					const div = new ViewContainer( 'div', null, b );
+				view: ( modelItem, 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( {
 			.add( downcastElementToElement( {
 				model: 'nested',
 				model: 'nested',
-				view: () => new ViewEditable( 'figcaption', { contenteditable: true } )
+				view: ( item, api ) => api.writer.createEditableElement( 'figcaption', { contenteditable: true } )
 			} ) );
 			} ) );
 
 
 		setData( editor.model,
 		setData( editor.model,

+ 27 - 0
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -740,6 +740,33 @@ describe( 'DocumentSelection', () => {
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
 			} );
 			} );
 		} );
 		} );
+
+		it( '`DocumentSelection#change:range` event should be fire once even if selection contains multi-ranges', () => {
+			root.removeChildren( 0, root.childCount );
+			root.insertChildren( 0, [
+				new Element( 'p', [], new Text( 'abcdef' ) ),
+				new Element( 'p', [], new Text( 'foobar' ) ),
+				new Text( 'xyz #2' )
+			] );
+
+			selection._setTo( [
+				Range.createIn( root.getNodeByPath( [ 0 ] ) ),
+				Range.createIn( root.getNodeByPath( [ 1 ] ) )
+			] );
+
+			spyRange = sinon.spy();
+			selection.on( 'change:range', spyRange );
+
+			model.applyOperation( wrapInDelta(
+				new InsertOperation(
+					new Position( root, [ 0 ] ),
+					'xyz #1',
+					doc.version
+				)
+			) );
+
+			expect( spyRange.calledOnce ).to.be.true;
+		} );
 	} );
 	} );
 
 
 	describe( 'attributes', () => {
 	describe( 'attributes', () => {

+ 123 - 30
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -6,6 +6,7 @@
 import MarkerCollection from '../../src/model/markercollection';
 import MarkerCollection from '../../src/model/markercollection';
 import Position from '../../src/model/position';
 import Position from '../../src/model/position';
 import Range from '../../src/model/range';
 import Range from '../../src/model/range';
+import LiveRange from '../../src/model/liverange';
 import Text from '../../src/model/text';
 import Text from '../../src/model/text';
 import Model from '../../src/model/model';
 import Model from '../../src/model/model';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -41,7 +42,7 @@ describe( 'MarkerCollection', () => {
 	} );
 	} );
 
 
 	describe( '_set', () => {
 	describe( '_set', () => {
-		it( 'should create a marker, fire set:<markerName> event and return true', () => {
+		it( 'should create a marker and fire update:<markerName>', () => {
 			sinon.spy( markers, 'fire' );
 			sinon.spy( markers, 'fire' );
 
 
 			const result = markers._set( 'name', range );
 			const result = markers._set( 'name', range );
@@ -49,47 +50,71 @@ describe( 'MarkerCollection', () => {
 
 
 			expect( result ).to.equal( marker );
 			expect( result ).to.equal( marker );
 			expect( marker.name ).to.equal( 'name' );
 			expect( marker.name ).to.equal( 'name' );
+			expect( marker.managedUsingOperations ).to.false;
 			expect( marker.getRange().isEqual( range ) ).to.be.true;
 			expect( marker.getRange().isEqual( range ) ).to.be.true;
-			expect( markers.fire.calledWithExactly( 'set:name', marker ) ).to.be.true;
+			sinon.assert.calledWithExactly( markers.fire, 'update:name', result, null, range );
 		} );
 		} );
 
 
-		it( 'should fire remove:<markerName> event, and create a new marker if marker with given name was in the collection', () => {
-			const marker1 = markers._set( 'name', range );
+		it( 'should create a marker marked as managed by operations', () => {
+			const marker = markers._set( 'name', range, true );
+
+			expect( marker.managedUsingOperations ).to.true;
+		} );
+
+		it( 'should update marker range and fire update:<markerName> event if marker with given name was in the collection', () => {
+			const marker = markers._set( 'name', range );
 
 
 			sinon.spy( markers, 'fire' );
 			sinon.spy( markers, 'fire' );
+			sinon.spy( marker, '_detachLiveRange' );
+			sinon.spy( marker, '_attachLiveRange' );
 
 
-			const marker2 = markers._set( 'name', range2 );
+			const result = markers._set( 'name', range2 );
 
 
-			expect( markers.fire.calledWithExactly( 'remove:name', marker1 ) ).to.be.true;
-			expect( markers.fire.calledWithExactly( 'set:name', marker2 ) ).to.be.true;
+			expect( result ).to.equal( marker );
+			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
+
+			sinon.assert.calledWithExactly( markers.fire, 'update:name', marker, range, range2 );
+			sinon.assert.calledOnce( marker._detachLiveRange );
+			sinon.assert.calledOnce( marker._detachLiveRange );
+		} );
 
 
-			expect( marker2.name ).to.equal( 'name' );
-			expect( marker2.getRange().isEqual( range2 ) ).to.be.true;
+		it( 'should update marker#managedUsingOperations and fire update:<markerName> event if marker with given name ' +
+			'was in the collection',
+		() => {
+			const marker = markers._set( 'name', range );
 
 
-			expect( marker1 ).not.to.equal( marker2 );
+			sinon.spy( markers, 'fire' );
+			sinon.spy( marker, '_detachLiveRange' );
+			sinon.spy( marker, '_attachLiveRange' );
+
+			const result = markers._set( 'name', range, true );
+
+			expect( result ).to.equal( marker );
+			expect( marker.managedUsingOperations ).to.true;
+			expect( marker.getRange().isEqual( range ) ).to.be.true;
+
+			sinon.assert.calledWithExactly( markers.fire, 'update:name', marker, range, range );
+			sinon.assert.notCalled( marker._detachLiveRange );
+			sinon.assert.notCalled( marker._attachLiveRange );
 		} );
 		} );
 
 
-		it( 'should not fire event and return the same marker if given marker has a range equal to given range', () => {
-			const marker1 = markers._set( 'name', range );
+		it( 'should not fire event if given marker has not changed', () => {
+			const marker = markers._set( 'name', range );
 
 
 			sinon.spy( markers, 'fire' );
 			sinon.spy( markers, 'fire' );
 
 
-			const marker2 = markers._set( 'name', range );
+			const result = markers._set( 'name', range );
 
 
-			expect( marker1 ).to.equal( marker2 );
-			expect( markers.fire.notCalled ).to.be.true;
+			expect( marker ).to.equal( result );
+			sinon.assert.notCalled( markers.fire );
 		} );
 		} );
 
 
 		it( 'should accept marker instance instead of name', () => {
 		it( 'should accept marker instance instead of name', () => {
-			markers._set( 'name', range );
-			const marker1 = markers.get( 'name' );
+			const marker = markers._set( 'name', range );
 
 
-			const result = markers._set( marker1, range2 );
-			const marker2 = markers.get( 'name' );
+			markers._set( marker, range2 );
 
 
-			expect( result ).to.equal( marker2 );
-			expect( marker2.getRange().isEqual( range2 ) );
-			expect( marker1 ).not.to.equal( marker2 );
+			expect( marker.getRange().isEqual( range2 ) ).to.true;
 		} );
 		} );
 	} );
 	} );
 
 
@@ -115,7 +140,7 @@ describe( 'MarkerCollection', () => {
 	} );
 	} );
 
 
 	describe( '_remove', () => {
 	describe( '_remove', () => {
-		it( 'should remove marker, return true and fire remove:<markerName> event', () => {
+		it( 'should remove marker, return true and fire update:<markerName> event', () => {
 			const marker = markers._set( 'name', range );
 			const marker = markers._set( 'name', range );
 
 
 			sinon.spy( markers, 'fire' );
 			sinon.spy( markers, 'fire' );
@@ -123,22 +148,20 @@ describe( 'MarkerCollection', () => {
 			const result = markers._remove( 'name' );
 			const result = markers._remove( 'name' );
 
 
 			expect( result ).to.be.true;
 			expect( result ).to.be.true;
-			expect( markers.fire.calledWithExactly( 'remove:name', marker ) ).to.be.true;
 			expect( markers.get( 'name' ) ).to.be.null;
 			expect( markers.get( 'name' ) ).to.be.null;
+			sinon.assert.calledWithExactly( markers.fire, 'update:name', marker, range, null );
 		} );
 		} );
 
 
 		it( 'should destroy marker instance', () => {
 		it( 'should destroy marker instance', () => {
 			const marker = markers._set( 'name', range );
 			const marker = markers._set( 'name', range );
-			const liveRange = marker._liveRange;
 
 
 			sinon.spy( marker, 'stopListening' );
 			sinon.spy( marker, 'stopListening' );
-			sinon.spy( liveRange, 'detach' );
+			sinon.spy( marker, '_detachLiveRange' );
 
 
 			markers._remove( 'name' );
 			markers._remove( 'name' );
 
 
 			expect( marker.stopListening.calledOnce ).to.be.true;
 			expect( marker.stopListening.calledOnce ).to.be.true;
-			expect( marker._liveRange ).to.be.null;
-			expect( liveRange.detach.calledOnce ).to.be.true;
+			expect( marker._detachLiveRange.calledOnce ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false if name has not been found in collection', () => {
 		it( 'should return false if name has not been found in collection', () => {
@@ -160,7 +183,7 @@ describe( 'MarkerCollection', () => {
 			const result = markers._remove( marker );
 			const result = markers._remove( marker );
 
 
 			expect( result ).to.be.true;
 			expect( result ).to.be.true;
-			expect( markers.fire.calledWithExactly( 'remove:name', marker ) ).to.be.true;
+			expect( markers.fire.calledWithExactly( 'update:name', marker, range, null ) ).to.be.true;
 			expect( markers.get( 'name' ) ).to.be.null;
 			expect( markers.get( 'name' ) ).to.be.null;
 		} );
 		} );
 	} );
 	} );
@@ -255,9 +278,13 @@ describe( 'Marker', () => {
 		expect( () => {
 		expect( () => {
 			marker.getEnd();
 			marker.getEnd();
 		} ).to.throw( CKEditorError, /^marker-destroyed/ );
 		} ).to.throw( CKEditorError, /^marker-destroyed/ );
+
+		expect( () => {
+			marker.managedUsingOperations;
+		} ).to.throw( CKEditorError, /^marker-destroyed/ );
 	} );
 	} );
 
 
-	it( 'should delegate events from live range', () => {
+	it( 'should attach live range to marker', () => {
 		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
 		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
 		const marker = model.markers._set( 'name', range );
 		const marker = model.markers._set( 'name', range );
 
 
@@ -273,4 +300,70 @@ describe( 'Marker', () => {
 		expect( eventRange.calledOnce ).to.be.true;
 		expect( eventRange.calledOnce ).to.be.true;
 		expect( eventContent.calledOnce ).to.be.true;
 		expect( eventContent.calledOnce ).to.be.true;
 	} );
 	} );
+
+	it( 'should detach live range from marker', () => {
+		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const marker = model.markers._set( 'name', range );
+		const liveRange = marker._liveRange;
+
+		const eventRange = sinon.spy();
+		const eventContent = sinon.spy();
+		sinon.spy( liveRange, 'detach' );
+
+		marker.on( 'change:range', eventRange );
+		marker.on( 'change:content', eventContent );
+
+		marker._detachLiveRange();
+
+		liveRange.fire( 'change:range', null, {} );
+		liveRange.fire( 'change:content', null, {} );
+
+		expect( eventRange.notCalled ).to.be.true;
+		expect( eventContent.notCalled ).to.be.true;
+		expect( liveRange.detach.calledOnce ).to.true;
+	} );
+
+	it( 'should reattach live range to marker', () => {
+		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const marker = model.markers._set( 'name', range );
+		const oldLiveRange = marker._liveRange;
+		const newLiveRange = LiveRange.createFromParentsAndOffsets( root, 0, root, 1 );
+
+		const eventRange = sinon.spy();
+		const eventContent = sinon.spy();
+		sinon.spy( oldLiveRange, 'detach' );
+
+		marker.on( 'change:range', eventRange );
+		marker.on( 'change:content', eventContent );
+
+		marker._attachLiveRange( newLiveRange );
+
+		oldLiveRange.fire( 'change:range', null, {} );
+		oldLiveRange.fire( 'change:content', null, {} );
+
+		expect( eventRange.notCalled ).to.be.true;
+		expect( eventContent.notCalled ).to.be.true;
+		expect( oldLiveRange.detach.calledOnce ).to.true;
+
+		newLiveRange.fire( 'change:range', null, {} );
+		newLiveRange.fire( 'change:content', null, {} );
+
+		expect( eventRange.calledOnce ).to.be.true;
+		expect( eventContent.calledOnce ).to.be.true;
+	} );
+
+	it( 'should change managedUsingOperations flag', () => {
+		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const marker = model.markers._set( 'name', range, false );
+
+		expect( marker.managedUsingOperations ).to.false;
+
+		marker._managedUsingOperations = true;
+
+		expect( marker.managedUsingOperations ).to.true;
+
+		marker._managedUsingOperations = false;
+
+		expect( marker.managedUsingOperations ).to.false;
+	} );
 } );
 } );

+ 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', () => {
 		describe( 'objects handling', () => {
 			beforeEach( () => {
 			beforeEach( () => {
 				model.schema.register( 'obj', {
 				model.schema.register( 'obj', {

+ 3 - 3
packages/ckeditor5-engine/tests/model/writer.js

@@ -1995,12 +1995,12 @@ describe( 'Writer', () => {
 		} );
 		} );
 
 
 		it( 'should update marker in the document marker collection', () => {
 		it( 'should update marker in the document marker collection', () => {
-			setMarker( 'name', range );
+			const marker = setMarker( 'name', range );
 
 
 			const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
 			const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 );
 			setMarker( 'name', range2 );
 			setMarker( 'name', range2 );
 
 
-			expect( model.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
+			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should accept marker instance', () => {
 		it( 'should accept marker instance', () => {
@@ -2013,7 +2013,7 @@ describe( 'Writer', () => {
 
 
 			const op = batch.deltas[ 1 ].operations[ 0 ];
 			const op = batch.deltas[ 1 ].operations[ 0 ];
 
 
-			expect( model.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true;
+			expect( marker.getRange().isEqual( range2 ) ).to.be.true;
 			expect( op.oldRange.isEqual( range ) ).to.be.true;
 			expect( op.oldRange.isEqual( range ) ).to.be.true;
 			expect( op.newRange.isEqual( range2 ) ).to.be.true;
 			expect( op.newRange.isEqual( range2 ) ).to.be.true;
 		} );
 		} );

+ 216 - 0
packages/ckeditor5-engine/tests/tickets/1281.js

@@ -0,0 +1,216 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Position from '../../src/model/position';
+
+import { setData as setModelData, getData as getModelData } from '../../src/dev-utils/model';
+
+describe( 'Bug ckeditor5-engine#1281', () => {
+	let element, editor, model;
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor
+			.create( element, { plugins: [ Paragraph ] } )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+			} );
+	} );
+
+	afterEach( () => {
+		element.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'loads content that contains multi-range selection', () => {
+		setModelData( model,
+			'<paragraph>Paragraph 1.</paragraph>' +
+			'<paragraph>Paragraph 2.</paragraph>' +
+			'<paragraph>[Paragraph 3.]</paragraph>' +
+			'<paragraph>[Paragraph 4.]</paragraph>'
+		);
+
+		const root = model.document.getRoot();
+		const thirdParagraph = root.getNodeByPath( [ 2 ] );
+		const fourthParagraph = root.getNodeByPath( [ 3 ] );
+		const selRanges = Array.from( model.document.selection.getRanges() );
+
+		expect( selRanges.length ).to.equal( 2 );
+
+		assertPositions( Position.createAt( thirdParagraph ), selRanges[ 0 ].start );
+		assertPositions( Position.createAt( thirdParagraph, 'end' ), selRanges[ 0 ].end );
+
+		assertPositions( Position.createAt( fourthParagraph ), selRanges[ 1 ].start );
+		assertPositions( Position.createAt( fourthParagraph, 'end' ), selRanges[ 1 ].end );
+	} );
+
+	it( 'does not throw an error when content before the selection is being removed (last element is selected)', () => {
+		setModelData( model,
+			'<paragraph>Paragraph 1.</paragraph>' +
+			'<paragraph>Paragraph 2.</paragraph>' +
+			'<paragraph>[Paragraph 3.]</paragraph>' +
+			'<paragraph>[Paragraph 4.]</paragraph>'
+		);
+
+		model.change( writer => {
+			const root = model.document.getRoot();
+			const firstParagraph = root.getNodeByPath( [ 0 ] );
+
+			expect( () => {
+				writer.remove( firstParagraph );
+			} ).to.not.throw();
+
+			assertOutput(
+				'<paragraph>Paragraph 2.</paragraph>' +
+				'<paragraph>[Paragraph 3.]</paragraph>' +
+				'<paragraph>[Paragraph 4.]</paragraph>'
+			);
+		} );
+	} );
+
+	it( 'does not throw an error when content before the selection is being removed (last element is not selected)', () => {
+		setModelData( model,
+			'<paragraph>Paragraph 1.</paragraph>' +
+			'<paragraph>Paragraph 2.</paragraph>' +
+			'<paragraph>[Paragraph 3.]</paragraph>' +
+			'<paragraph>[Paragraph 4.]</paragraph>' +
+			'<paragraph>Paragraph 5.</paragraph>'
+		);
+
+		model.change( writer => {
+			const root = model.document.getRoot();
+			const firstParagraph = root.getNodeByPath( [ 0 ] );
+
+			expect( () => {
+				writer.remove( firstParagraph );
+			} ).to.not.throw();
+
+			assertOutput(
+				'<paragraph>Paragraph 2.</paragraph>' +
+				'<paragraph>[Paragraph 3.]</paragraph>' +
+				'<paragraph>[Paragraph 4.]</paragraph>' +
+				'<paragraph>Paragraph 5.</paragraph>'
+			);
+		} );
+	} );
+
+	it( 'does not throw an error when content after the selection is being removed (first element is selected)', () => {
+		setModelData( model,
+			'<paragraph>[Paragraph 1.]</paragraph>' +
+			'<paragraph>Paragraph 2.</paragraph>' +
+			'<paragraph>Paragraph 3.</paragraph>' +
+			'<paragraph>[Paragraph 4.]</paragraph>' +
+			'<paragraph>Paragraph 5.</paragraph>'
+		);
+
+		model.change( writer => {
+			const root = model.document.getRoot();
+			const lastParagraph = root.getNodeByPath( [ 4 ] );
+
+			expect( () => {
+				writer.remove( lastParagraph );
+			} ).to.not.throw();
+
+			assertOutput(
+				'<paragraph>[Paragraph 1.]</paragraph>' +
+				'<paragraph>Paragraph 2.</paragraph>' +
+				'<paragraph>Paragraph 3.</paragraph>' +
+				'<paragraph>[Paragraph 4.]</paragraph>'
+			);
+		} );
+	} );
+
+	it( 'does not throw an error when content after the selection is being removed (first element is not selected)', () => {
+		setModelData( model,
+			'<paragraph>Paragraph 1.</paragraph>' +
+			'<paragraph>Paragraph 2.</paragraph>' +
+			'<paragraph>[Paragraph 3.]</paragraph>' +
+			'<paragraph>[Paragraph 4.]</paragraph>' +
+			'<paragraph>Paragraph 5.</paragraph>'
+		);
+
+		model.change( writer => {
+			const root = model.document.getRoot();
+			const lastParagraph = root.getNodeByPath( [ 4 ] );
+
+			expect( () => {
+				writer.remove( lastParagraph );
+			} ).to.not.throw();
+
+			assertOutput(
+				'<paragraph>Paragraph 1.</paragraph>' +
+				'<paragraph>Paragraph 2.</paragraph>' +
+				'<paragraph>[Paragraph 3.]</paragraph>' +
+				'<paragraph>[Paragraph 4.]</paragraph>'
+			);
+		} );
+	} );
+
+	it( 'does not throw an error when content between the selection\'s ranges is being removed (last element is selected)', () => {
+		setModelData( model,
+			'<paragraph>Paragraph 1.</paragraph>' +
+			'<paragraph>[Paragraph 2.]</paragraph>' +
+			'<paragraph>Paragraph 3.</paragraph>' +
+			'<paragraph>[Paragraph 4.]</paragraph>'
+		);
+
+		model.change( writer => {
+			const root = model.document.getRoot();
+			const thirdParagraph = root.getNodeByPath( [ 2 ] );
+
+			expect( () => {
+				writer.remove( thirdParagraph );
+			} ).to.not.throw();
+
+			assertOutput(
+				'<paragraph>Paragraph 1.</paragraph>' +
+				'<paragraph>[Paragraph 2.]</paragraph>' +
+				'<paragraph>[Paragraph 4.]</paragraph>'
+			);
+		} );
+	} );
+
+	it( 'does not throw an error when content between the selection\'s ranges is being removed (last element is not selected)', () => {
+		setModelData( model,
+			'<paragraph>Paragraph 1.</paragraph>' +
+			'<paragraph>[Paragraph 2.]</paragraph>' +
+			'<paragraph>Paragraph 3.</paragraph>' +
+			'<paragraph>[Paragraph 4.]</paragraph>' +
+			'<paragraph>Paragraph 5.</paragraph>'
+		);
+
+		model.change( writer => {
+			const root = model.document.getRoot();
+			const thirdParagraph = root.getNodeByPath( [ 2 ] );
+
+			expect( () => {
+				writer.remove( thirdParagraph );
+			} ).to.not.throw();
+
+			assertOutput(
+				'<paragraph>Paragraph 1.</paragraph>' +
+				'<paragraph>[Paragraph 2.]</paragraph>' +
+				'<paragraph>[Paragraph 4.]</paragraph>' +
+				'<paragraph>Paragraph 5.</paragraph>'
+			);
+		} );
+	} );
+
+	function assertPositions( firstPosition, secondPosition ) {
+		expect( firstPosition.isEqual( secondPosition ) ).to.be.true;
+	}
+
+	function assertOutput( output ) {
+		expect( getModelData( model ) ).to.equal( output );
+	}
+} );

+ 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' ) {
 export default function createRoot( doc, name = 'div', rootName = 'main' ) {
 	const root = new RootEditableElement( name );
 	const root = new RootEditableElement( name );
 
 
-	root.document = doc;
+	root._document = doc;
 	root.rootName = rootName;
 	root.rootName = rootName;
 	doc.roots.add( root );
 	doc.roots.add( root );
 
 

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

@@ -51,7 +51,7 @@ describe( 'AttributeElement', () => {
 	describe( 'clone', () => {
 	describe( 'clone', () => {
 		it( 'should clone element with priority', () => {
 		it( 'should clone element with priority', () => {
 			const el = new AttributeElement( 'b' );
 			const el = new AttributeElement( 'b' );
-			el.priority = 7;
+			el._priority = 7;
 
 
 			const clone = el.clone();
 			const clone = el.clone();
 
 
@@ -64,17 +64,17 @@ describe( 'AttributeElement', () => {
 	describe( 'isSimilar', () => {
 	describe( 'isSimilar', () => {
 		it( 'should return true if priorities are the same', () => {
 		it( 'should return true if priorities are the same', () => {
 			const b1 = new AttributeElement( 'b' );
 			const b1 = new AttributeElement( 'b' );
-			b1.priority = 7;
+			b1._priority = 7;
 
 
 			const b2 = new AttributeElement( 'b' );
 			const b2 = new AttributeElement( 'b' );
-			b2.priority = 7;
+			b2._priority = 7;
 
 
 			expect( b1.isSimilar( b2 ) ).to.be.true;
 			expect( b1.isSimilar( b2 ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false if priorities are different', () => {
 		it( 'should return false if priorities are different', () => {
 			const b1 = new AttributeElement( 'b' );
 			const b1 = new AttributeElement( 'b' );
-			b1.priority = 7;
+			b1._priority = 7;
 
 
 			const b2 = new AttributeElement( 'b' ); // default priority
 			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', () => {
 		it( 'should keep a copy of selection', () => {
 			const selectionCopy = new ViewSelection( 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 );
 			const bindSelection = converter.fakeSelectionToView( domEl );
 
 
 			expect( bindSelection ).to.not.equal( selection );
 			expect( bindSelection ).to.not.equal( selection );

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

@@ -40,7 +40,7 @@ describe( 'DomConverter', () => {
 		beforeEach( () => {
 		beforeEach( () => {
 			viewDocument = new ViewDocument();
 			viewDocument = new ViewDocument();
 			viewEditable = new ViewEditable( 'div' );
 			viewEditable = new ViewEditable( 'div' );
-			viewEditable.document = viewDocument;
+			viewEditable._document = viewDocument;
 
 
 			domEditable = document.createElement( 'div' );
 			domEditable = document.createElement( 'div' );
 			domEditableParent = document.createElement( 'div' );
 			domEditableParent = document.createElement( 'div' );
@@ -53,7 +53,6 @@ describe( 'DomConverter', () => {
 		afterEach( () => {
 		afterEach( () => {
 			converter.unbindDomElement( domEditable );
 			converter.unbindDomElement( domEditable );
 			document.body.removeChild( domEditableParent );
 			document.body.removeChild( domEditableParent );
-			viewDocument.destroy();
 
 
 			document.body.focus();
 			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', () => {
 		it( 'should create tree of DOM elements from view elements', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
 			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( viewImg );
 			viewP.appendChildren( viewText );
 			viewP.appendChildren( viewText );
@@ -59,9 +57,7 @@ describe( 'DomConverter', () => {
 		it( 'should create tree of DOM elements from view elements and bind elements', () => {
 		it( 'should create tree of DOM elements from view elements and bind elements', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
 			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( viewImg );
 			viewP.appendChildren( viewText );
 			viewP.appendChildren( viewText );
@@ -98,9 +94,7 @@ describe( 'DomConverter', () => {
 		it( 'should create tree of DOM elements from view element without children', () => {
 		it( 'should create tree of DOM elements from view element without children', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
 			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( viewImg );
 			viewP.appendChildren( viewText );
 			viewP.appendChildren( viewText );

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

@@ -18,7 +18,7 @@ describe( 'EditableElement', () => {
 		} );
 		} );
 
 
 		it( 'should allow to set document', () => {
 		it( 'should allow to set document', () => {
-			element.document = docMock;
+			element._document = docMock;
 
 
 			expect( element.document ).to.equal( docMock );
 			expect( element.document ).to.equal( docMock );
 		} );
 		} );
@@ -28,16 +28,16 @@ describe( 'EditableElement', () => {
 		} );
 		} );
 
 
 		it( 'should throw if trying to set document again', () => {
 		it( 'should throw if trying to set document again', () => {
-			element.document = docMock;
+			element._document = docMock;
 			const newDoc = createDocumentMock();
 			const newDoc = createDocumentMock();
 
 
 			expect( () => {
 			expect( () => {
-				element.document = newDoc;
+				element._document = newDoc;
 			} ).to.throw( CKEditorError, 'view-editableelement-document-already-set: View document is already set.' );
 			} ).to.throw( CKEditorError, 'view-editableelement-document-already-set: View document is already set.' );
 		} );
 		} );
 
 
 		it( 'should be cloned properly', () => {
 		it( 'should be cloned properly', () => {
-			element.document = docMock;
+			element._document = docMock;
 			const newElement = element.clone();
 			const newElement = element.clone();
 
 
 			expect( newElement.document ).to.equal( docMock );
 			expect( newElement.document ).to.equal( docMock );
@@ -51,16 +51,16 @@ describe( 'EditableElement', () => {
 			docMock = createDocumentMock();
 			docMock = createDocumentMock();
 
 
 			viewMain = new RootEditableElement( 'div' );
 			viewMain = new RootEditableElement( 'div' );
-			viewMain.document = docMock;
+			viewMain._document = docMock;
 
 
 			viewHeader = new RootEditableElement( 'h1' );
 			viewHeader = new RootEditableElement( 'h1' );
-			viewHeader.document = docMock;
+			viewHeader._document = docMock;
 			viewHeader.rootName = 'header';
 			viewHeader.rootName = 'header';
 		} );
 		} );
 
 
 		it( 'should be observable', () => {
 		it( 'should be observable', () => {
 			const root = new RootEditableElement( 'div' );
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 
 			expect( root.isFocused ).to.be.false;
 			expect( root.isFocused ).to.be.false;
 
 
@@ -75,48 +75,25 @@ describe( 'EditableElement', () => {
 			expect( isFocusedSpy.calledOnce ).to.be.true;
 			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 rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
-			docMock.selection.setTo( rangeMain );
+			docMock.selection._setTo( rangeMain );
 			docMock.isFocused = true;
 			docMock.isFocused = true;
 
 
 			expect( viewMain.isFocused ).to.be.true;
 			expect( viewMain.isFocused ).to.be.true;
 			expect( viewHeader.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.false;
 
 
-			docMock.selection.setTo( [ rangeHeader ] );
-			docMock.fire( 'render' );
+			docMock.selection._setTo( [ rangeHeader ] );
 
 
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.true;
 			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', () => {
 		it( 'should change isFocused when document.isFocus changes', () => {
 			const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
 			const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
-			docMock.selection.setTo( rangeMain );
+			docMock.selection._setTo( rangeMain );
 			docMock.isFocused = true;
 			docMock.isFocused = true;
 
 
 			expect( viewMain.isFocused ).to.be.true;
 			expect( viewMain.isFocused ).to.be.true;
@@ -127,7 +104,7 @@ describe( 'EditableElement', () => {
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.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( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.false;
@@ -137,7 +114,7 @@ describe( 'EditableElement', () => {
 	describe( 'isReadOnly', () => {
 	describe( 'isReadOnly', () => {
 		it( 'should be observable', () => {
 		it( 'should be observable', () => {
 			const root = new RootEditableElement( 'div' );
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 
 			expect( root.isReadOnly ).to.be.false;
 			expect( root.isReadOnly ).to.be.false;
 
 
@@ -154,7 +131,7 @@ describe( 'EditableElement', () => {
 
 
 		it( 'should be bound to the document#isReadOnly', () => {
 		it( 'should be bound to the document#isReadOnly', () => {
 			const root = new RootEditableElement( 'div' );
 			const root = new RootEditableElement( 'div' );
-			root.document = createDocumentMock();
+			root._document = createDocumentMock();
 
 
 			root.document.isReadOnly = false;
 			root.document.isReadOnly = false;
 
 
@@ -170,7 +147,7 @@ describe( 'EditableElement', () => {
 		it( 'should return document', () => {
 		it( 'should return document', () => {
 			const docMock = createDocumentMock();
 			const docMock = createDocumentMock();
 			const root = new RootEditableElement( 'div' );
 			const root = new RootEditableElement( 'div' );
-			root.document = docMock;
+			root._document = docMock;
 
 
 			expect( root.document ).to.equal( 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', () => {
 		it( 'should clone class attribute', () => {
 			const el = new Element( 'p', { foo: 'bar' } );
 			const el = new Element( 'p', { foo: 'bar' } );
-			el.addClass( 'baz', 'qux' );
+			el._addClass( [ 'baz', 'qux' ] );
 			const clone = el.clone( false );
 			const clone = el.clone( false );
 
 
 			expect( clone ).to.not.equal( el );
 			expect( clone ).to.not.equal( el );
@@ -198,8 +198,8 @@ describe( 'Element', () => {
 		it( 'should clone custom properties', () => {
 		it( 'should clone custom properties', () => {
 			const el = new Element( 'p' );
 			const el = new Element( 'p' );
 			const symbol = Symbol( 'custom' );
 			const symbol = Symbol( 'custom' );
-			el.setCustomProperty( 'foo', 'bar' );
-			el.setCustomProperty( symbol, 'baz' );
+			el._setCustomProperty( 'foo', 'bar' );
+			el._setCustomProperty( symbol, 'baz' );
 
 
 			const cloned = el.clone();
 			const cloned = el.clone();
 
 
@@ -247,9 +247,9 @@ describe( 'Element', () => {
 			const other1 = el.clone();
 			const other1 = el.clone();
 			const other2 = el.clone();
 			const other2 = el.clone();
 			const other3 = 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( other1 ) ).to.be.false;
 			expect( el.isSimilar( other2 ) ).to.be.false;
 			expect( el.isSimilar( other2 ) ).to.be.false;
 			expect( el.isSimilar( other3 ) ).to.be.false;
 			expect( el.isSimilar( other3 ) ).to.be.false;
@@ -261,10 +261,10 @@ describe( 'Element', () => {
 			const el3 = new Element( 'p' );
 			const el3 = new Element( 'p' );
 			const el4 = 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( el2 ) ).to.be.true;
 			expect( el1.isSimilar( el3 ) ).to.be.false;
 			expect( el1.isSimilar( el3 ) ).to.be.false;
@@ -277,13 +277,13 @@ describe( 'Element', () => {
 			const el3 = new Element( 'p' );
 			const el3 = new Element( 'p' );
 			const el4 = 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( el2 ) ).to.be.false;
 			expect( el1.isSimilar( el3 ) ).to.be.true;
 			expect( el1.isSimilar( el3 ) ).to.be.true;
@@ -430,16 +430,16 @@ describe( 'Element', () => {
 			el = new Element( 'p' );
 			el = new Element( 'p' );
 		} );
 		} );
 
 
-		describe( 'setAttribute', () => {
+		describe( '_setAttribute', () => {
 			it( 'should set attribute', () => {
 			it( 'should set attribute', () => {
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 
 
 				expect( el._attrs.has( 'foo' ) ).to.be.true;
 				expect( el._attrs.has( 'foo' ) ).to.be.true;
 				expect( el._attrs.get( 'foo' ) ).to.equal( 'bar' );
 				expect( el._attrs.get( 'foo' ) ).to.equal( 'bar' );
 			} );
 			} );
 
 
 			it( 'should cast attribute value to a string', () => {
 			it( 'should cast attribute value to a string', () => {
-				el.setAttribute( 'foo', true );
+				el._setAttribute( 'foo', true );
 
 
 				expect( el._attrs.get( 'foo' ) ).to.equal( 'true' );
 				expect( el._attrs.get( 'foo' ) ).to.equal( 'true' );
 			} );
 			} );
@@ -450,11 +450,11 @@ describe( 'Element', () => {
 					done();
 					done();
 				} );
 				} );
 
 
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 			} );
 			} );
 
 
 			it( 'should set class', () => {
 			it( 'should set class', () => {
-				el.setAttribute( 'class', 'foo bar' );
+				el._setAttribute( 'class', 'foo bar' );
 
 
 				expect( el._attrs.has( 'class' ) ).to.be.false;
 				expect( el._attrs.has( 'class' ) ).to.be.false;
 				expect( el._classes.has( 'foo' ) ).to.be.true;
 				expect( el._classes.has( 'foo' ) ).to.be.true;
@@ -462,8 +462,8 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should replace all existing classes', () => {
 			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( 'foo' ) ).to.be.false;
 				expect( el._classes.has( 'bar' ) ).to.be.false;
 				expect( el._classes.has( 'bar' ) ).to.be.false;
@@ -472,9 +472,9 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should replace all styles', () => {
 			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( 'color' ) ).to.be.false;
 				expect( el.hasStyle( 'top' ) ).to.be.false;
 				expect( el.hasStyle( 'top' ) ).to.be.false;
@@ -485,14 +485,14 @@ describe( 'Element', () => {
 
 
 		describe( 'getAttribute', () => {
 		describe( 'getAttribute', () => {
 			it( 'should return attribute', () => {
 			it( 'should return attribute', () => {
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 
 
 				expect( el.getAttribute( 'foo' ) ).to.equal( 'bar' );
 				expect( el.getAttribute( 'foo' ) ).to.equal( 'bar' );
 				expect( el.getAttribute( 'bom' ) ).to.not.be.ok;
 				expect( el.getAttribute( 'bom' ) ).to.not.be.ok;
 			} );
 			} );
 
 
 			it( 'should return class attribute', () => {
 			it( 'should return class attribute', () => {
-				el.addClass( 'foo', 'bar' );
+				el._addClass( [ 'foo', 'bar' ] );
 
 
 				expect( el.getAttribute( 'class' ) ).to.equal( 'foo bar' );
 				expect( el.getAttribute( 'class' ) ).to.equal( 'foo bar' );
 			} );
 			} );
@@ -502,8 +502,8 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should return style attribute', () => {
 			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;' );
 				expect( el.getAttribute( 'style' ) ).to.equal( 'color:red;top:10px;' );
 			} );
 			} );
@@ -515,17 +515,17 @@ describe( 'Element', () => {
 
 
 		describe( 'getAttributes', () => {
 		describe( 'getAttributes', () => {
 			it( 'should return attributes', () => {
 			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' ] ] );
 				expect( Array.from( el.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'abc', 'xyz' ] ] );
 			} );
 			} );
 
 
 			it( 'should return class and style attribute', () => {
 			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( [
 				expect( Array.from( el.getAttributes() ) ).to.deep.equal( [
 					[ 'class', 'abc xyz' ], [ 'style', 'width:20px;font-weight:bold;' ]
 					[ 'class', 'abc xyz' ], [ 'style', 'width:20px;font-weight:bold;' ]
@@ -535,7 +535,7 @@ describe( 'Element', () => {
 
 
 		describe( 'hasAttribute', () => {
 		describe( 'hasAttribute', () => {
 			it( 'should return true if element has attribute', () => {
 			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( 'foo' ) ).to.be.true;
 				expect( el.hasAttribute( 'bom' ) ).to.be.false;
 				expect( el.hasAttribute( 'bom' ) ).to.be.false;
@@ -543,21 +543,21 @@ describe( 'Element', () => {
 
 
 			it( 'should return true if element has class attribute', () => {
 			it( 'should return true if element has class attribute', () => {
 				expect( el.hasAttribute( 'class' ) ).to.be.false;
 				expect( el.hasAttribute( 'class' ) ).to.be.false;
-				el.addClass( 'foo' );
+				el._addClass( 'foo' );
 				expect( el.hasAttribute( 'class' ) ).to.be.true;
 				expect( el.hasAttribute( 'class' ) ).to.be.true;
 			} );
 			} );
 
 
 			it( 'should return true if element has style attribute', () => {
 			it( 'should return true if element has style attribute', () => {
 				expect( el.hasAttribute( 'style' ) ).to.be.false;
 				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;
 				expect( el.hasAttribute( 'style' ) ).to.be.true;
 			} );
 			} );
 		} );
 		} );
 
 
 		describe( 'getAttributeKeys', () => {
 		describe( 'getAttributeKeys', () => {
 			it( 'should return keys', () => {
 			it( 'should return keys', () => {
-				el.setAttribute( 'foo', true );
-				el.setAttribute( 'bar', true );
+				el._setAttribute( 'foo', true );
+				el._setAttribute( 'bar', true );
 
 
 				const expected = [ 'foo', 'bar' ];
 				const expected = [ 'foo', 'bar' ];
 				let i = 0;
 				let i = 0;
@@ -571,8 +571,8 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should return class key', () => {
 			it( 'should return class key', () => {
-				el.addClass( 'foo' );
-				el.setAttribute( 'bar', true );
+				el._addClass( 'foo' );
+				el._setAttribute( 'bar', true );
 				const expected = [ 'class', 'bar' ];
 				const expected = [ 'class', 'bar' ];
 				let i = 0;
 				let i = 0;
 
 
@@ -583,8 +583,8 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should return style key', () => {
 			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' ];
 				const expected = [ 'style', 'bar' ];
 				let i = 0;
 				let i = 0;
 
 
@@ -595,13 +595,13 @@ describe( 'Element', () => {
 			} );
 			} );
 		} );
 		} );
 
 
-		describe( 'removeAttribute', () => {
+		describe( '_removeAttribute', () => {
 			it( 'should remove attributes', () => {
 			it( 'should remove attributes', () => {
-				el.setAttribute( 'foo', true );
+				el._setAttribute( 'foo', true );
 
 
 				expect( el.hasAttribute( 'foo' ) ).to.be.true;
 				expect( el.hasAttribute( 'foo' ) ).to.be.true;
 
 
-				el.removeAttribute( 'foo' );
+				el._removeAttribute( 'foo' );
 
 
 				expect( el.hasAttribute( 'foo' ) ).to.be.false;
 				expect( el.hasAttribute( 'foo' ) ).to.be.false;
 
 
@@ -609,20 +609,20 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should fire change event with attributes type', done => {
 			it( 'should fire change event with attributes type', done => {
-				el.setAttribute( 'foo', 'bar' );
+				el._setAttribute( 'foo', 'bar' );
 				el.once( 'change:attributes', eventInfo => {
 				el.once( 'change:attributes', eventInfo => {
 					expect( eventInfo.source ).to.equal( el );
 					expect( eventInfo.source ).to.equal( el );
 					done();
 					done();
 				} );
 				} );
 
 
-				el.removeAttribute( 'foo' );
+				el._removeAttribute( 'foo' );
 			} );
 			} );
 
 
 			it( 'should remove class attribute', () => {
 			it( 'should remove class attribute', () => {
-				el.addClass( 'foo', 'bar' );
+				el._addClass( [ 'foo', 'bar' ] );
 				const el2 = new Element( 'p' );
 				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.hasAttribute( 'class' ) ).to.be.false;
 				expect( el.hasClass( 'foo' ) ).to.be.false;
 				expect( el.hasClass( 'foo' ) ).to.be.false;
@@ -632,11 +632,11 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should remove style attribute', () => {
 			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 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.hasAttribute( 'style' ) ).to.be.false;
 				expect( el.hasStyle( 'color' ) ).to.be.false;
 				expect( el.hasStyle( 'color' ) ).to.be.false;
@@ -654,9 +654,9 @@ describe( 'Element', () => {
 			el = new Element( 'p' );
 			el = new Element( 'p' );
 		} );
 		} );
 
 
-		describe( 'addClass', () => {
+		describe( '_addClass()', () => {
 			it( 'should add single class', () => {
 			it( 'should add single class', () => {
-				el.addClass( 'one' );
+				el._addClass( 'one' );
 
 
 				expect( el._classes.has( 'one' ) ).to.be.true;
 				expect( el._classes.has( 'one' ) ).to.be.true;
 			} );
 			} );
@@ -667,11 +667,11 @@ describe( 'Element', () => {
 					done();
 					done();
 				} );
 				} );
 
 
-				el.addClass( 'one' );
+				el._addClass( 'one' );
 			} );
 			} );
 
 
 			it( 'should add multiple classes', () => {
 			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( 'one' ) ).to.be.true;
 				expect( el._classes.has( 'two' ) ).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', () => {
 			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( 'one' ) ).to.be.false;
 				expect( el._classes.has( 'two' ) ).to.be.true;
 				expect( el._classes.has( 'two' ) ).to.be.true;
@@ -691,18 +691,18 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should fire change event with attributes type', done => {
 			it( 'should fire change event with attributes type', done => {
-				el.addClass( 'one' );
+				el._addClass( 'one' );
 				el.once( 'change:attributes', eventInfo => {
 				el.once( 'change:attributes', eventInfo => {
 					expect( eventInfo.source ).to.equal( el );
 					expect( eventInfo.source ).to.equal( el );
 					done();
 					done();
 				} );
 				} );
 
 
-				el.removeClass( 'one' );
+				el._removeClass( 'one' );
 			} );
 			} );
 
 
 			it( 'should remove multiple classes', () => {
 			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( 'one' ) ).to.be.false;
 				expect( el._classes.has( 'two' ) ).to.be.false;
 				expect( el._classes.has( 'two' ) ).to.be.false;
@@ -713,7 +713,7 @@ describe( 'Element', () => {
 
 
 		describe( 'hasClass', () => {
 		describe( 'hasClass', () => {
 			it( 'should check if element has a class', () => {
 			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( 'one' ) ).to.be.true;
 				expect( el.hasClass( 'two' ) ).to.be.true;
 				expect( el.hasClass( 'two' ) ).to.be.true;
@@ -722,7 +722,7 @@ describe( 'Element', () => {
 			} );
 			} );
 
 
 			it( 'should check if element has multiple classes', () => {
 			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( 'one', 'two' ) ).to.be.true;
 				expect( el.hasClass( 'three', 'two' ) ).to.be.true;
 				expect( el.hasClass( 'three', 'two' ) ).to.be.true;
@@ -734,7 +734,7 @@ describe( 'Element', () => {
 		describe( 'getClassNames', () => {
 		describe( 'getClassNames', () => {
 			it( 'should return iterator with all class names', () => {
 			it( 'should return iterator with all class names', () => {
 				const names = [ 'one', 'two', 'three' ];
 				const names = [ 'one', 'two', 'three' ];
-				el.addClass( ...names );
+				el._addClass( names );
 				const iterator = el.getClassNames();
 				const iterator = el.getClassNames();
 				let i = 0;
 				let i = 0;
 
 
@@ -752,9 +752,9 @@ describe( 'Element', () => {
 			el = new Element( 'p' );
 			el = new Element( 'p' );
 		} );
 		} );
 
 
-		describe( 'setStyle', () => {
+		describe( '_setStyle()', () => {
 			it( 'should set element style', () => {
 			it( 'should set element style', () => {
-				el.setStyle( 'color', 'red' );
+				el._setStyle( 'color', 'red' );
 
 
 				expect( el._styles.has( 'color' ) ).to.be.true;
 				expect( el._styles.has( 'color' ) ).to.be.true;
 				expect( el._styles.get( 'color' ) ).to.equal( 'red' );
 				expect( el._styles.get( 'color' ) ).to.equal( 'red' );
@@ -766,11 +766,11 @@ describe( 'Element', () => {
 					done();
 					done();
 				} );
 				} );
 
 
-				el.setStyle( 'color', 'red' );
+				el._setStyle( 'color', 'red' );
 			} );
 			} );
 
 
 			it( 'should set multiple styles by providing an object', () => {
 			it( 'should set multiple styles by providing an object', () => {
-				el.setStyle( {
+				el._setStyle( {
 					color: 'red',
 					color: 'red',
 					position: 'fixed'
 					position: 'fixed'
 				} );
 				} );
@@ -784,7 +784,7 @@ describe( 'Element', () => {
 
 
 		describe( 'getStyle', () => {
 		describe( 'getStyle', () => {
 			it( 'should get style', () => {
 			it( 'should get style', () => {
-				el.setStyle( {
+				el._setStyle( {
 					color: 'red',
 					color: 'red',
 					border: '1px solid red'
 					border: '1px solid red'
 				} );
 				} );
@@ -798,7 +798,7 @@ describe( 'Element', () => {
 			it( 'should return iterator with all style names', () => {
 			it( 'should return iterator with all style names', () => {
 				const names = [ 'color', 'position' ];
 				const names = [ 'color', 'position' ];
 
 
-				el.setStyle( {
+				el._setStyle( {
 					color: 'red',
 					color: 'red',
 					position: 'absolute'
 					position: 'absolute'
 				} );
 				} );
@@ -814,14 +814,14 @@ describe( 'Element', () => {
 
 
 		describe( 'hasStyle', () => {
 		describe( 'hasStyle', () => {
 			it( 'should check if element has a style', () => {
 			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-top' ) ).to.be.true;
 				expect( el.hasStyle( 'padding-left' ) ).to.be.false;
 				expect( el.hasStyle( 'padding-left' ) ).to.be.false;
 			} );
 			} );
 
 
 			it( 'should check if element has multiple styles', () => {
 			it( 'should check if element has multiple styles', () => {
-				el.setStyle( {
+				el._setStyle( {
 					'padding-top': '10px',
 					'padding-top': '10px',
 					'margin-left': '10px',
 					'margin-left': '10px',
 					'color': '10px;'
 					'color': '10px;'
@@ -833,31 +833,31 @@ describe( 'Element', () => {
 			} );
 			} );
 		} );
 		} );
 
 
-		describe( 'removeStyle', () => {
+		describe( '_removeStyle()', () => {
 			it( 'should remove style', () => {
 			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;
 				expect( el.hasStyle( 'padding-top' ) ).to.be.false;
 			} );
 			} );
 
 
 			it( 'should fire change event with attributes type', done => {
 			it( 'should fire change event with attributes type', done => {
-				el.setStyle( 'color', 'red' );
+				el._setStyle( 'color', 'red' );
 				el.once( 'change:attributes', eventInfo => {
 				el.once( 'change:attributes', eventInfo => {
 					expect( eventInfo.source ).to.equal( el );
 					expect( eventInfo.source ).to.equal( el );
 					done();
 					done();
 				} );
 				} );
 
 
-				el.removeStyle( 'color' );
+				el._removeStyle( 'color' );
 			} );
 			} );
 
 
 			it( 'should remove multiple styles', () => {
 			it( 'should remove multiple styles', () => {
-				el.setStyle( {
+				el._setStyle( {
 					'padding-top': '10px',
 					'padding-top': '10px',
 					'margin-top': '10px',
 					'margin-top': '10px',
 					'color': 'red'
 					'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( 'padding-top' ) ).to.be.false;
 				expect( el.hasStyle( 'margin-top' ) ).to.be.false;
 				expect( el.hasStyle( 'margin-top' ) ).to.be.false;
@@ -964,7 +964,7 @@ describe( 'Element', () => {
 	describe( 'custom properties', () => {
 	describe( 'custom properties', () => {
 		it( 'should allow to set and get custom properties', () => {
 		it( 'should allow to set and get custom properties', () => {
 			const el = new Element( 'p' );
 			const el = new Element( 'p' );
-			el.setCustomProperty( 'foo', 'bar' );
+			el._setCustomProperty( 'foo', 'bar' );
 
 
 			expect( el.getCustomProperty( 'foo' ) ).to.equal( 'bar' );
 			expect( el.getCustomProperty( 'foo' ) ).to.equal( 'bar' );
 		} );
 		} );
@@ -972,7 +972,7 @@ describe( 'Element', () => {
 		it( 'should allow to add symbol property', () => {
 		it( 'should allow to add symbol property', () => {
 			const el = new Element( 'p' );
 			const el = new Element( 'p' );
 			const symbol = Symbol( 'custom' );
 			const symbol = Symbol( 'custom' );
-			el.setCustomProperty( symbol, 'bar' );
+			el._setCustomProperty( symbol, 'bar' );
 
 
 			expect( el.getCustomProperty( symbol ) ).to.equal( 'bar' );
 			expect( el.getCustomProperty( symbol ) ).to.equal( 'bar' );
 		} );
 		} );
@@ -980,14 +980,14 @@ describe( 'Element', () => {
 		it( 'should allow to remove custom property', () => {
 		it( 'should allow to remove custom property', () => {
 			const el = new Element( 'foo' );
 			const el = new Element( 'foo' );
 			const symbol = Symbol( 'quix' );
 			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( 'bar' ) ).to.equal( 'baz' );
 			expect( el.getCustomProperty( symbol ) ).to.equal( 'test' );
 			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( 'bar' ) ).to.be.undefined;
 			expect( el.getCustomProperty( symbol ) ).to.be.undefined;
 			expect( el.getCustomProperty( symbol ) ).to.be.undefined;
@@ -995,9 +995,9 @@ describe( 'Element', () => {
 
 
 		it( 'should allow to iterate over custom properties', () => {
 		it( 'should allow to iterate over custom properties', () => {
 			const el = new Element( 'p' );
 			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() ];
 			const properties = [ ...el.getCustomProperties() ];
 
 
@@ -1019,7 +1019,7 @@ describe( 'Element', () => {
 
 
 		it( 'should return classes in sorted order', () => {
 		it( 'should return classes in sorted order', () => {
 			const el = new Element( 'fruit' );
 			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"' );
 			expect( el.getIdentity() ).to.equal( 'fruit class="apple,banana,lemon"' );
 		} );
 		} );
@@ -1049,7 +1049,7 @@ describe( 'Element', () => {
 				style: 'text-align:center;border-radius:10px'
 				style: 'text-align:center;border-radius:10px'
 			} );
 			} );
 
 
-			el.addClass( 'three', 'two', 'one' );
+			el._addClass( [ 'three', 'two', 'one' ] );
 
 
 			expect( el.getIdentity() ).to.equal(
 			expect( el.getIdentity() ).to.equal(
 				'baz class="one,three,two" style="border-radius:10px;text-align:center" bar="two" foo="one"'
 				'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 */
 /* globals console, document */
 
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import createViewRoot from '../_utils/createroot';
 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 {
 class ClickObserver1 extends DomEventObserver {
-	constructor( viewDocument ) {
-		super( viewDocument );
+	constructor( view ) {
+		super( view );
 
 
 		this.id = 1;
 		this.id = 1;
 		this.domEventType = 'click';
 		this.domEventType = 'click';
@@ -25,8 +29,8 @@ class ClickObserver1 extends DomEventObserver {
 }
 }
 
 
 class ClickObserver2 extends DomEventObserver {
 class ClickObserver2 extends DomEventObserver {
-	constructor( viewDocument ) {
-		super( viewDocument );
+	constructor( view ) {
+		super( view );
 
 
 		this.id = 2;
 		this.id = 2;
 		this.domEventType = 'click';
 		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 ) );
 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.
 // Random order.
-viewDocument.addObserver( ClickObserver1 );
-
+view.addObserver( ClickObserver1 );
 createViewRoot( viewDocument, 'div', 'clickerA' );
 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' );
 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 */
 /* globals document, console */
 
 
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import DomEventObserver from '../../../src/view/observer/domeventobserver';
 import ViewRange from '../../../src/view/range';
 import ViewRange from '../../../src/view/range';
 import createViewRoot from '../_utils/createroot';
 import createViewRoot from '../_utils/createroot';
 import { setData } from '../../../src/dev-utils/view';
 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 domEditable = document.getElementById( 'editor' );
-const viewRoot = createViewRoot();
+const viewRoot = createViewRoot( viewDocument );
 let viewStrong;
 let viewStrong;
 
 
-viewDocument.attachDomRoot( domEditable );
+view.attachDomRoot( domEditable );
 
 
 // Add mouseup oberver.
 // Add mouseup oberver.
-viewDocument.addObserver( class extends DomEventObserver {
+view.addObserver( class extends DomEventObserver {
 	get domEventType() {
 	get domEventType() {
 		return [ 'mousedown', 'mouseup' ];
 		return [ 'mousedown', 'mouseup' ];
 	}
 	}
@@ -30,19 +31,19 @@ viewDocument.addObserver( class extends DomEventObserver {
 } );
 } );
 
 
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 viewDocument.on( 'selectionChange', ( evt, data ) => {
-	viewDocument.selection.setTo( data.newSelection );
-	viewDocument.render();
+	view.change( writer => {
+		writer.setSelection( data.newSelection );
+	} );
 } );
 } );
 
 
 viewDocument.on( 'mouseup', ( evt, data ) => {
 viewDocument.on( 'mouseup', ( evt, data ) => {
 	if ( data.target == viewStrong ) {
 	if ( data.target == viewStrong ) {
 		console.log( 'Making selection around the <strong>.' );
 		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();
 		data.preventDefault();
 	}
 	}
@@ -57,28 +58,26 @@ viewDocument.selection.on( 'change', () => {
 	const lastPos = viewDocument.selection.getLastPosition();
 	const lastPos = viewDocument.selection.getLastPosition();
 
 
 	if ( firstPos && lastPos && firstPos.nodeAfter == viewStrong && lastPos.nodeBefore == viewStrong ) {
 	if ( firstPos && lastPos && firstPos.nodeAfter == viewStrong && lastPos.nodeBefore == viewStrong ) {
-		viewStrong.addClass( 'selected' );
+		view.change( writer => writer.addClass( 'selected', viewStrong ) );
 	} else {
 	} else {
-		viewStrong.removeClass( 'selected' );
+		view.change( writer => writer.removeClass( 'selected', viewStrong ) );
 	}
 	}
 } );
 } );
 
 
 viewDocument.on( 'focus', () => {
 viewDocument.on( 'focus', () => {
-	viewStrong.addClass( 'focused' );
-	viewDocument.render();
+	view.change( writer => writer.addClass( 'focused', viewStrong ) );
 
 
 	console.log( 'The document was focused.' );
 	console.log( 'The document was focused.' );
 } );
 } );
 
 
 viewDocument.on( 'blur', () => {
 viewDocument.on( 'blur', () => {
-	viewStrong.removeClass( 'focused' );
-	viewDocument.render();
+	view.change( writer => writer.removeClass( 'focused', viewStrong ) );
 
 
 	console.log( 'The document was blurred.' );
 	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 );
 const viewP = viewRoot.getChild( 0 );
 viewStrong = viewP.getChild( 1 );
 viewStrong = viewP.getChild( 1 );
 
 
-viewDocument.focus();
+view.focus();

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

@@ -5,32 +5,46 @@
 
 
 /* globals document */
 /* 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 domEditable1 = document.getElementById( 'editable1' );
 const domEditable2 = document.getElementById( 'editable2' );
 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', () => {
 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', () => {
 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 */
 /* 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( 'focus', ( evt, data ) => console.log( `Focus in ${ data.domTarget.id }.` ) );
 viewDocument.on( 'blur', ( evt, data ) => console.log( `Blur 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 domEditable1 = document.getElementById( 'editable1' );
 const domEditable2 = document.getElementById( 'editable2' );
 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.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', () => {
 editable1.on( 'change:isFocused', () => {
 	domEditable1.style.backgroundColor = editable1.isFocused ? 'green' : 'red';
 	domEditable1.style.backgroundColor = editable1.isFocused ? 'green' : 'red';
 } );
 } );
+
 editable2.on( 'change:isFocused', () => {
 editable2.on( 'change:isFocused', () => {
 	domEditable2.style.backgroundColor = editable2.isFocused ? 'green' : 'red';
 	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 */
 /* globals document */
 
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/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><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' +
 	'<container:p></container:p>' +
 	'<container:p></container:p>' +
 	'<container:p><attribute:strong></attribute:strong></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 */
 /* 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';
 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.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 */
 /* 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( 'keydown', ( evt, data ) => console.log( 'keydown', data ) );
 viewDocument.on( 'keyup', ( evt, data ) => console.log( 'keyup', 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 */
 /* 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';
 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( 'mutations', ( evt, mutations ) => console.log( mutations ) );
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 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>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 */
 /* globals document */
 
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/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' );
 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>foo</container:p>' +
 	'<container:p>bar</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 */
 /* globals document */
 
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/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>foo</container:p>' +
 	'<container:p>bar</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 */
 /* globals console, document */
 
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/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><attribute:b>foo</attribute:b>bar</container:p>' +
 	'<container:p>bom</container:p>' );
 	'<container:p>bom</container:p>' );
 
 
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 	console.log( 'selectionChange', data );
 	console.log( 'selectionChange', data );
-	viewDocument.selection.setTo( data.newSelection );
+	view.change( writer => writer.setSelection( data.newSelection ) );
 } );
 } );
 
 
 viewDocument.on( 'selectionChangeDone', ( evt, data ) => {
 viewDocument.on( 'selectionChangeDone', ( evt, data ) => {
 	console.log( '%c selectionChangeDone ', 'background: #222; color: #bada55', data );
 	console.log( '%c selectionChangeDone ', 'background: #222; color: #bada55', data );
-	viewDocument.selection.setTo( data.newSelection );
+	view.change( writer => writer.setSelection( data.newSelection ) );
 } );
 } );
-
-viewDocument.render();

+ 16 - 21
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 Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
-import UIElement from '../../../src/view/uielement';
 import Position from '../../../src/view/position';
 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 );
 		const root = this.toDomElement( domDocument );
 		root.classList.add( 'ui-element' );
 		root.classList.add( 'ui-element' );
 		root.innerHTML = 'END OF PARAGRAPH';
 		root.innerHTML = 'END OF PARAGRAPH';
 
 
 		return root;
 		return root;
-	};
+	} );
 
 
 	return element;
 	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 );
 		const root = this.toDomElement( domDocument );
 		root.classList.add( 'ui-element' );
 		root.classList.add( 'ui-element' );
 		root.innerHTML = 'X';
 		root.innerHTML = 'X';
 
 
 		return root;
 		return root;
-	};
+	} );
 
 
 	return element;
 	return element;
 }
 }
@@ -51,9 +45,9 @@ class UIElementTestPlugin extends Plugin {
 		const editing = editor.editing;
 		const editing = editor.editing;
 
 
 		// Add some UIElement to each paragraph.
 		// Add some UIElement to each paragraph.
-		editing.downcastDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
+		editing.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
 			const viewP = conversionApi.mapper.toViewElement( data.item );
 			const viewP = conversionApi.mapper.toViewElement( data.item );
-			viewP.appendChildren( createEndingUIElement() );
+			viewP.appendChildren( createEndingUIElement( conversionApi.writer ) );
 		}, { priority: 'lowest' } );
 		}, { priority: 'lowest' } );
 	}
 	}
 }
 }
@@ -65,18 +59,19 @@ ClassicEditor
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
 		window.editor = editor;
 		window.editor = editor;
+		const view = editor.editing.view;
 
 
 		// Add some UI elements.
 		// 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 viewText1 = viewRoot.getChild( 0 ).getChild( 0 );
 		const viewText2 = viewRoot.getChild( 1 ).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 => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

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

@@ -5,24 +5,26 @@
 
 
 /* globals console, document */
 /* globals console, document */
 
 
-import Document from '../../../src/view/document';
+import View from '../../../src/view/view';
 import { setData } from '../../../src/dev-utils/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>fo{}o</container:p>' +
 	'<container:p></container:p>' +
 	'<container:p></container:p>' +
 	'<container:p><attribute:strong></attribute:strong></container:p>' +
 	'<container:p><attribute:strong></attribute:strong></container:p>' +
 	'<container:p>bar</container:p>' );
 	'<container:p>bar</container:p>' );
 
 
+view.focus();
+
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 viewDocument.on( 'selectionChange', ( evt, data ) => {
 	const node = data.newSelection.getFirstPosition().parent;
 	const node = data.newSelection.getFirstPosition().parent;
 	console.log( node.name ? node.name : node._data );
 	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 matcher = new Matcher( pattern );
 			const el = new Element( 'a' );
 			const el = new Element( 'a' );
-			el.addClass( 'foo', 'bar', 'baz' );
+			el._addClass( [ 'foo', 'bar', 'baz' ] );
 
 
 			const result = matcher.match( el );
 			const result = matcher.match( el );
 			expect( result ).to.be.an( 'object' );
 			expect( result ).to.be.an( 'object' );
@@ -328,7 +328,7 @@ describe( 'Matcher', () => {
 			};
 			};
 			const matcher = new Matcher( pattern );
 			const matcher = new Matcher( pattern );
 			const el = new Element( 'a' );
 			const el = new Element( 'a' );
-			el.setStyle( {
+			el._setStyle( {
 				color: 'red',
 				color: 'red',
 				position: 'relative'
 				position: 'relative'
 			} );
 			} );
@@ -376,9 +376,9 @@ describe( 'Matcher', () => {
 			const el2 = new Element( 'p' );
 			const el2 = new Element( 'p' );
 			const el3 = 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 );
 			const result = matcher.matchAll( el1, el2, el3 );
 			expect( result ).to.be.an( 'array' );
 			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', () => {
 		it( 'should return Document attached to the parent element', () => {
 			const docMock = createDocumentMock();
 			const docMock = createDocumentMock();
 			const parent = new RootEditableElement( 'div' );
 			const parent = new RootEditableElement( 'div' );
-			parent.document = docMock;
+			parent._document = docMock;
 			const child = new Element( 'p' );
 			const child = new Element( 'p' );
 
 
 			child.parent = parent;
 			child.parent = parent;
@@ -248,7 +248,7 @@ describe( 'Node', () => {
 
 
 		it( 'should return root element', () => {
 		it( 'should return root element', () => {
 			const parent = new RootEditableElement( 'div' );
 			const parent = new RootEditableElement( 'div' );
-			parent.document = createDocumentMock();
+			parent._document = createDocumentMock();
 			const child = new Element( 'p' );
 			const child = new Element( 'p' );
 
 
 			child.parent = parent;
 			child.parent = parent;
@@ -303,8 +303,7 @@ describe( 'Node', () => {
 
 
 		beforeEach( () => {
 		beforeEach( () => {
 			text = new Text( 'foo' );
 			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 = new Element( 'p', { renderer: { markToSync: rootChangeSpy } } );
 			root.appendChildren( [ text, img ] );
 			root.appendChildren( [ text, img ] );
@@ -323,31 +322,31 @@ describe( 'Node', () => {
 				imgChangeSpy( 'attributes', node );
 				imgChangeSpy( 'attributes', node );
 			} );
 			} );
 
 
-			img.setAttribute( 'width', 100 );
+			img._setAttribute( 'width', 100 );
 
 
 			sinon.assert.calledOnce( imgChangeSpy );
 			sinon.assert.calledOnce( imgChangeSpy );
 			sinon.assert.calledWith( imgChangeSpy, 'attributes', img );
 			sinon.assert.calledWith( imgChangeSpy, 'attributes', img );
 		} );
 		} );
 
 
 		it( 'should be fired on the parent', () => {
 		it( 'should be fired on the parent', () => {
-			img.setAttribute( 'width', 100 );
+			img._setAttribute( 'width', 100 );
 
 
 			sinon.assert.calledOnce( rootChangeSpy );
 			sinon.assert.calledOnce( rootChangeSpy );
 			sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 			sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 		} );
 		} );
 
 
-		describe( 'setAttribute()', () => {
+		describe( '_setAttribute()', () => {
 			it( 'should fire change event', () => {
 			it( 'should fire change event', () => {
-				img.setAttribute( 'width', 100 );
+				img._setAttribute( 'width', 100 );
 
 
 				sinon.assert.calledOnce( rootChangeSpy );
 				sinon.assert.calledOnce( rootChangeSpy );
 				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 			} );
 			} );
 		} );
 		} );
 
 
-		describe( 'removeAttribute()', () => {
+		describe( '_removeAttribute()', () => {
 			it( 'should fire change event', () => {
 			it( 'should fire change event', () => {
-				img.removeAttribute( 'src' );
+				img._removeAttribute( 'src' );
 
 
 				sinon.assert.calledOnce( rootChangeSpy );
 				sinon.assert.calledOnce( rootChangeSpy );
 				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );
 				sinon.assert.calledWith( rootChangeSpy, 'attributes', img );

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

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

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

@@ -6,31 +6,33 @@
 /* globals document */
 /* globals document */
 
 
 import DomEventData from '../../../src/view/observer/domeventdata';
 import DomEventData from '../../../src/view/observer/domeventdata';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 
 
 describe( 'DomEventData', () => {
 describe( 'DomEventData', () => {
-	let viewDocument, viewBody, domRoot;
+	let view, viewDocument, viewBody, domRoot;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 
 
 		domRoot = document.createElement( 'div' );
 		domRoot = document.createElement( 'div' );
 		domRoot.innerHTML = '<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>';
 		domRoot.innerHTML = '<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>';
 		document.body.appendChild( domRoot );
 		document.body.appendChild( domRoot );
 
 
-		viewBody = viewDocument.domConverter.domToView( document.body, { bind: true } );
+		viewBody = view.domConverter.domToView( document.body, { bind: true } );
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
 		domRoot.parentElement.removeChild( domRoot );
 		domRoot.parentElement.removeChild( domRoot );
-		viewDocument.destroy();
+		view.destroy();
 	} );
 	} );
 
 
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'sets properties', () => {
 		it( 'sets properties', () => {
 			const domEvt = { target: document.body };
 			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( 'document', viewDocument );
 			expect( data ).to.have.property( 'domEvent', domEvt );
 			expect( data ).to.have.property( 'domEvent', domEvt );
 			expect( data ).to.have.property( 'domTarget', document.body );
 			expect( data ).to.have.property( 'domTarget', document.body );
@@ -43,7 +45,7 @@ describe( 'DomEventData', () => {
 	describe( 'target', () => {
 	describe( 'target', () => {
 		it( 'returns bound element', () => {
 		it( 'returns bound element', () => {
 			const domEvt = { target: document.body };
 			const domEvt = { target: document.body };
-			const data = new DomEventData( viewDocument, domEvt );
+			const data = new DomEventData( view, domEvt );
 
 
 			expect( data ).to.have.property( 'target', viewBody );
 			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 DomEventObserver from '../../../src/view/observer/domeventobserver';
 import Observer from '../../../src/view/observer/observer';
 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 UIElement from '../../../src/view/uielement';
 import createViewRoot from '../_utils/createroot';
 import createViewRoot from '../_utils/createroot';
 
 
 class ClickObserver extends DomEventObserver {
 class ClickObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		this.domEventType = 'click';
 		this.domEventType = 'click';
 	}
 	}
@@ -24,8 +24,8 @@ class ClickObserver extends DomEventObserver {
 }
 }
 
 
 class MultiObserver extends DomEventObserver {
 class MultiObserver extends DomEventObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		this.domEventType = [ 'evt1', 'evt2' ];
 		this.domEventType = [ 'evt1', 'evt2' ];
 	}
 	}
@@ -36,22 +36,23 @@ class MultiObserver extends DomEventObserver {
 }
 }
 
 
 class ClickCapturingObserver extends ClickObserver {
 class ClickCapturingObserver extends ClickObserver {
-	constructor( document ) {
-		super( document );
+	constructor( view ) {
+		super( view );
 
 
 		this.useCapture = true;
 		this.useCapture = true;
 	}
 	}
 }
 }
 
 
 describe( 'DomEventObserver', () => {
 describe( 'DomEventObserver', () => {
-	let viewDocument;
+	let view, viewDocument;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 	} );
 
 
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
@@ -68,8 +69,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 		const evtSpy = sinon.spy();
 
 
 		createViewRoot( viewDocument );
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		viewDocument.addObserver( ClickObserver );
+		view.attachDomRoot( domElement );
+		view.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 		viewDocument.on( 'click', evtSpy );
 
 
 		domElement.dispatchEvent( domEvent );
 		domElement.dispatchEvent( domEvent );
@@ -91,8 +92,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy2 = sinon.spy();
 		const evtSpy2 = sinon.spy();
 
 
 		createViewRoot( viewDocument );
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		viewDocument.addObserver( MultiObserver );
+		view.attachDomRoot( domElement );
+		view.addObserver( MultiObserver );
 		viewDocument.on( 'evt1', evtSpy1 );
 		viewDocument.on( 'evt1', evtSpy1 );
 		viewDocument.on( 'evt2', evtSpy2 );
 		viewDocument.on( 'evt2', evtSpy2 );
 
 
@@ -109,8 +110,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 		const evtSpy = sinon.spy();
 
 
 		createViewRoot( viewDocument );
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		const testObserver = viewDocument.addObserver( ClickObserver );
+		view.attachDomRoot( domElement );
+		const testObserver = view.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 		viewDocument.on( 'click', evtSpy );
 
 
 		testObserver.disable();
 		testObserver.disable();
@@ -126,8 +127,8 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 		const evtSpy = sinon.spy();
 
 
 		createViewRoot( viewDocument );
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		const testObserver = viewDocument.addObserver( ClickObserver );
+		view.attachDomRoot( domElement );
+		const testObserver = view.addObserver( ClickObserver );
 		viewDocument.on( 'click', evtSpy );
 		viewDocument.on( 'click', evtSpy );
 
 
 		testObserver.disable();
 		testObserver.disable();
@@ -149,8 +150,8 @@ describe( 'DomEventObserver', () => {
 		const domEvent = new MouseEvent( 'click' );
 		const domEvent = new MouseEvent( 'click' );
 		domElement.appendChild( childDomElement );
 		domElement.appendChild( childDomElement );
 		createViewRoot( viewDocument );
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domElement );
-		viewDocument.addObserver( ClickCapturingObserver );
+		view.attachDomRoot( domElement );
+		view.addObserver( ClickCapturingObserver );
 
 
 		viewDocument.on( 'click', ( evt, domEventData ) => {
 		viewDocument.on( 'click', ( evt, domEventData ) => {
 			expect( domEventData.domEvent.eventPhase ).to.equal( domEventData.domEvent.CAPTURING_PHASE );
 			expect( domEventData.domEvent.eventPhase ).to.equal( domEventData.domEvent.CAPTURING_PHASE );
@@ -179,14 +180,14 @@ describe( 'DomEventObserver', () => {
 		beforeEach( () => {
 		beforeEach( () => {
 			domRoot = document.createElement( 'div' );
 			domRoot = document.createElement( 'div' );
 			const viewRoot = createViewRoot( viewDocument );
 			const viewRoot = createViewRoot( viewDocument );
-			viewDocument.attachDomRoot( domRoot );
+			view.attachDomRoot( domRoot );
 			uiElement = createUIElement( 'p' );
 			uiElement = createUIElement( 'p' );
 			viewRoot.appendChildren( uiElement );
 			viewRoot.appendChildren( uiElement );
-			viewDocument.render();
+			view.render();
 
 
 			domEvent = new MouseEvent( 'click', { bubbles: true } );
 			domEvent = new MouseEvent( 'click', { bubbles: true } );
 			evtSpy = sinon.spy();
 			evtSpy = sinon.spy();
-			viewDocument.addObserver( ClickObserver );
+			view.addObserver( ClickObserver );
 			viewDocument.on( 'click', evtSpy );
 			viewDocument.on( 'click', evtSpy );
 		} );
 		} );
 
 
@@ -213,7 +214,7 @@ describe( 'DomEventObserver', () => {
 
 
 	describe( 'fire', () => {
 	describe( 'fire', () => {
 		it( 'should do nothing if observer is disabled', () => {
 		it( 'should do nothing if observer is disabled', () => {
-			const testObserver = new ClickObserver( viewDocument );
+			const testObserver = new ClickObserver( view );
 			const fireSpy = sinon.spy( viewDocument, 'fire' );
 			const fireSpy = sinon.spy( viewDocument, 'fire' );
 
 
 			testObserver.disable();
 			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 createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
 import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
 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 DomEventData from '../../../src/view/observer/domeventdata';
 import createViewRoot from '../_utils/createroot';
 import createViewRoot from '../_utils/createroot';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData, stringify } from '../../../src/dev-utils/view';
 import { setData, stringify } from '../../../src/dev-utils/view';
 
 
 describe( 'FakeSelectionObserver', () => {
 describe( 'FakeSelectionObserver', () => {
-	let observer, viewDocument, root, domRoot;
+	let observer, view, viewDocument, root, domRoot;
 
 
 	before( () => {
 	before( () => {
 		domRoot = createElement( document, 'div', {
 		domRoot = createElement( document, 'div', {
@@ -28,19 +28,20 @@ describe( 'FakeSelectionObserver', () => {
 	} );
 	} );
 
 
 	beforeEach( () => {
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		root = createViewRoot( viewDocument );
 		root = createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domRoot );
-		observer = viewDocument.getObserver( FakeSelectionObserver );
-		viewDocument.selection.setFake();
+		view.attachDomRoot( domRoot );
+		observer = view.getObserver( FakeSelectionObserver );
+		viewDocument.selection._setFake();
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
-		viewDocument.destroy();
+		view.destroy();
 	} );
 	} );
 
 
 	it( 'should do nothing if selection is not fake', () => {
 	it( 'should do nothing if selection is not fake', () => {
-		viewDocument.selection.setFake( false );
+		viewDocument.selection._setFake( false );
 
 
 		return checkEventPrevention( keyCodes.arrowleft, false );
 		return checkEventPrevention( keyCodes.arrowleft, false );
 	} );
 	} );
@@ -190,7 +191,7 @@ describe( 'FakeSelectionObserver', () => {
 				resolve();
 				resolve();
 			} );
 			} );
 
 
-			setData( viewDocument, initialData );
+			setData( view, initialData );
 			changeFakeSelectionPressing( keyCode );
 			changeFakeSelectionPressing( keyCode );
 		} );
 		} );
 	}
 	}
@@ -199,7 +200,7 @@ describe( 'FakeSelectionObserver', () => {
 	//
 	//
 	// @param {Number} keyCode
 	// @param {Number} keyCode
 	function changeFakeSelectionPressing( keyCode ) {
 	function changeFakeSelectionPressing( keyCode ) {
-		viewDocument.selection.setFake();
+		viewDocument.selection._setFake();
 
 
 		const data = {
 		const data = {
 			keyCode,
 			keyCode,

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

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

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

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

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

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

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

@@ -5,30 +5,31 @@
 
 
 /* globals document */
 /* globals document */
 
 
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
 import MutationObserver from '../../../src/view/observer/mutationobserver';
 import MutationObserver from '../../../src/view/observer/mutationobserver';
 import UIElement from '../../../src/view/uielement';
 import UIElement from '../../../src/view/uielement';
 import createViewRoot from '../_utils/createroot';
 import createViewRoot from '../_utils/createroot';
 import { parse } from '../../../src/dev-utils/view';
 import { parse } from '../../../src/dev-utils/view';
 
 
 describe( 'MutationObserver', () => {
 describe( 'MutationObserver', () => {
-	let domEditor, viewDocument, viewRoot, mutationObserver, lastMutations, domRoot;
+	let view, domEditor, viewDocument, viewRoot, mutationObserver, lastMutations, domRoot;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		domRoot = document.createElement( 'div' );
 		domRoot = document.createElement( 'div' );
 		domRoot.innerHTML = '<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>';
 		domRoot.innerHTML = '<div contenteditable="true" id="main"></div><div contenteditable="true" id="additional"></div>';
 		document.body.appendChild( domRoot );
 		document.body.appendChild( domRoot );
 
 
-		viewDocument = new ViewDocument();
+		view = new View();
+		viewDocument = view.document;
 		domEditor = document.getElementById( 'main' );
 		domEditor = document.getElementById( 'main' );
 		lastMutations = null;
 		lastMutations = null;
 
 
 		createViewRoot( viewDocument );
 		createViewRoot( viewDocument );
-		viewDocument.attachDomRoot( domEditor );
-		viewDocument.selection.setTo( null );
+		view.attachDomRoot( domEditor );
+		viewDocument.selection._setTo( null );
 		document.getSelection().removeAllRanges();
 		document.getSelection().removeAllRanges();
 
 
-		mutationObserver = viewDocument.getObserver( MutationObserver );
+		mutationObserver = view.getObserver( MutationObserver );
 
 
 		viewDocument.on( 'mutations', ( evt, mutations ) => {
 		viewDocument.on( 'mutations', ( evt, mutations ) => {
 			lastMutations = mutations;
 			lastMutations = mutations;
@@ -38,14 +39,14 @@ describe( 'MutationObserver', () => {
 
 
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 	} );
 	} );
 
 
 	afterEach( () => {
 	afterEach( () => {
 		mutationObserver.disable();
 		mutationObserver.disable();
 
 
 		domRoot.parentElement.removeChild( domRoot );
 		domRoot.parentElement.removeChild( domRoot );
-		viewDocument.destroy();
+		view.destroy();
 	} );
 	} );
 
 
 	it( 'should handle typing', () => {
 	it( 'should handle typing', () => {
@@ -65,7 +66,7 @@ describe( 'MutationObserver', () => {
 		const additional = document.getElementById( 'additional' );
 		const additional = document.getElementById( 'additional' );
 		mutationObserver.disable();
 		mutationObserver.disable();
 		createViewRoot( viewDocument, 'div', 'additional' );
 		createViewRoot( viewDocument, 'div', 'additional' );
-		viewDocument.attachDomRoot( additional, 'additional' );
+		view.attachDomRoot( additional, 'additional' );
 
 
 		additional.textContent = 'foobar';
 		additional.textContent = 'foobar';
 		mutationObserver.flush();
 		mutationObserver.flush();
@@ -97,7 +98,7 @@ describe( 'MutationObserver', () => {
 	it( 'should handle unbold', () => {
 	it( 'should handle unbold', () => {
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 		viewRoot.appendChildren( parse( '<container:p><attribute:b>foo</attribute:b></container:p>' ) );
 		viewRoot.appendChildren( parse( '<container:p><attribute:b>foo</attribute:b></container:p>' ) );
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 0 ];
 		const domP = domEditor.childNodes[ 0 ];
 		const domB = domP.childNodes[ 0 ];
 		const domB = domP.childNodes[ 0 ];
@@ -200,13 +201,13 @@ describe( 'MutationObserver', () => {
 
 
 		// Prepare AdditionalEditor
 		// Prepare AdditionalEditor
 		createViewRoot( viewDocument, 'div', 'additional' );
 		createViewRoot( viewDocument, 'div', 'additional' );
-		viewDocument.attachDomRoot( domAdditionalEditor, 'additional' );
+		view.attachDomRoot( domAdditionalEditor, 'additional' );
 
 
 		viewDocument.getRoot( 'additional' ).appendChildren(
 		viewDocument.getRoot( 'additional' ).appendChildren(
 			parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 			parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 
 
 		// Render AdditionalEditor (first editor has been rendered in the beforeEach function)
 		// Render AdditionalEditor (first editor has been rendered in the beforeEach function)
-		viewDocument.render();
+		view.render();
 
 
 		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
 		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
 		domAdditionalEditor.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', () => {
 	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 ];
 		const inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
 		inlineFiller.data += 'x';
 		inlineFiller.data += 'x';
@@ -242,19 +243,20 @@ describe( 'MutationObserver', () => {
 	} );
 	} );
 
 
 	it( 'should have no inline filler in mutation', () => {
 	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 ];
 		let inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
 		inlineFiller.data += 'x';
 		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 = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
 		inlineFiller.data += 'y';
 		inlineFiller.data += 'y';
@@ -270,7 +272,7 @@ describe( 'MutationObserver', () => {
 	it( 'should have no block filler in mutation', () => {
 	it( 'should have no block filler in mutation', () => {
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		domP.removeChild( domP.childNodes[ 0 ] );
 		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', () => {
 	it( 'should ignore mutation with bogus br inserted on the end of the empty paragraph', () => {
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'br' ) );
 		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', () => {
 	it( 'should ignore mutation with bogus br inserted on the end of the paragraph with text', () => {
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'br' ) );
 		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', () => {
 	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>' ) );
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		domP.childNodes[ 0 ].data = 'foo ';
 		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', () => {
 	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>' ) );
 		viewRoot.appendChildren( parse( '<container:p><container:x></container:x></container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		const domY = document.createElement( 'y' );
 		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', () => {
 	it( 'should not ignore mutation with br inserted not on the end of the paragraph', () => {
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		domP.insertBefore( document.createElement( 'br' ), domP.childNodes[ 0 ] );
 		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', () => {
 	it( 'should not ignore mutation inserting element different than br on the end of the empty paragraph', () => {
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'span' ) );
 		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', () => {
 	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>' ) );
 		viewRoot.appendChildren( parse( '<container:p>foo</container:p>' ) );
 
 
-		viewDocument.render();
+		view.render();
 
 
 		const domP = domEditor.childNodes[ 2 ];
 		const domP = domEditor.childNodes[ 2 ];
 		domP.appendChild( document.createElement( 'span' ) );
 		domP.appendChild( document.createElement( 'span' ) );
@@ -437,7 +439,7 @@ describe( 'MutationObserver', () => {
 			const uiElement = createUIElement( 'div' );
 			const uiElement = createUIElement( 'div' );
 			viewRoot.appendChildren( uiElement );
 			viewRoot.appendChildren( uiElement );
 
 
-			viewDocument.render();
+			view.render();
 		} );
 		} );
 
 
 		it( 'should not collect text mutations from UIElement', () => {
 		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 Observer from '../../../src/view/observer/observer';
+import View from '../../../src/view/view';
 
 
 describe( 'Observer', () => {
 describe( 'Observer', () => {
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'should create Observer with properties', () => {
 		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.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;
 			expect( observer ).to.have.property( 'isEnabled' ).that.is.false;
 		} );
 		} );
 	} );
 	} );

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů