Преглед изворни кода

Merge branch 'master' into t/396

Maciej Gołaszewski пре 9 година
родитељ
комит
ca47f8c097
100 измењених фајлова са 5820 додато и 1599 уклоњено
  1. 0 0
      packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc
  2. 0 0
      packages/ckeditor5-engine/src/engine.jsdoc
  3. 2 2
      packages/ckeditor5-engine/src/treecontroller/editingcontroller.js
  4. 30 2
      packages/ckeditor5-engine/src/treecontroller/mapper.js
  5. 11 6
      packages/ckeditor5-engine/src/treecontroller/model-converter-builder.js
  6. 210 0
      packages/ckeditor5-engine/src/treecontroller/model-selection-to-view-converters.js
  7. 21 14
      packages/ckeditor5-engine/src/treecontroller/modelconsumable.js
  8. 70 0
      packages/ckeditor5-engine/src/treecontroller/modelconversiondispatcher.js
  9. 0 0
      packages/ckeditor5-engine/src/treecontroller/treecontroller.jsdoc
  10. 1 1
      packages/ckeditor5-engine/src/treemodel/operation/moveoperation.js
  11. 23 11
      packages/ckeditor5-engine/src/treemodel/selection.js
  12. 0 0
      packages/ckeditor5-engine/src/treemodel/treemodel.jsdoc
  13. 43 10
      packages/ckeditor5-engine/src/treeview/attributeelement.js
  14. 14 5
      packages/ckeditor5-engine/src/treeview/containerelement.js
  15. 70 42
      packages/ckeditor5-engine/src/treeview/document.js
  16. 289 57
      packages/ckeditor5-engine/src/treeview/domconverter.js
  17. 2 2
      packages/ckeditor5-engine/src/treeview/element.js
  18. 176 0
      packages/ckeditor5-engine/src/treeview/filler.js
  19. 15 15
      packages/ckeditor5-engine/src/treeview/node.js
  20. 7 7
      packages/ckeditor5-engine/src/treeview/observer/domeventdata.js
  21. 3 3
      packages/ckeditor5-engine/src/treeview/observer/domeventobserver.js
  22. 5 5
      packages/ckeditor5-engine/src/treeview/observer/focusobserver.js
  23. 5 5
      packages/ckeditor5-engine/src/treeview/observer/keyobserver.js
  24. 48 32
      packages/ckeditor5-engine/src/treeview/observer/mutationobserver.js
  25. 8 8
      packages/ckeditor5-engine/src/treeview/observer/observer.js
  26. 131 0
      packages/ckeditor5-engine/src/treeview/observer/selectionobserver.js
  27. 46 4
      packages/ckeditor5-engine/src/treeview/position.js
  28. 309 56
      packages/ckeditor5-engine/src/treeview/renderer.js
  29. 51 10
      packages/ckeditor5-engine/src/treeview/selection.js
  30. 0 0
      packages/ckeditor5-engine/src/treeview/treeview.jsdoc
  31. 73 38
      packages/ckeditor5-engine/src/treeview/writer.js
  32. 151 74
      packages/ckeditor5-engine/tests/_utils-tests/model.js
  33. 104 2
      packages/ckeditor5-engine/tests/_utils-tests/view.js
  34. 122 27
      packages/ckeditor5-engine/tests/_utils/model.js
  35. 94 17
      packages/ckeditor5-engine/tests/_utils/view.js
  36. 2 0
      packages/ckeditor5-engine/tests/dataprocessor/basichtmlwriter.js
  37. 3 1
      packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js
  38. 17 17
      packages/ckeditor5-engine/tests/treecontroller/mapper.js
  39. 73 2
      packages/ckeditor5-engine/tests/treecontroller/model-converter-builder.js
  40. 472 0
      packages/ckeditor5-engine/tests/treecontroller/model-selection-to-view-converters.js
  41. 72 0
      packages/ckeditor5-engine/tests/treecontroller/modelconversiondispatcher.js
  42. 6 6
      packages/ckeditor5-engine/tests/treemodel/composer/composer.js
  43. 2 2
      packages/ckeditor5-engine/tests/treemodel/composer/deletecontents.js
  44. 2 2
      packages/ckeditor5-engine/tests/treemodel/composer/modifyselection.js
  45. 9 11
      packages/ckeditor5-engine/tests/treemodel/delta/transform/_utils/utils.js
  46. 19 0
      packages/ckeditor5-engine/tests/treemodel/operation/moveoperation.js
  47. 24 3
      packages/ckeditor5-engine/tests/treemodel/selection.js
  48. 47 3
      packages/ckeditor5-engine/tests/treeview/attributeelement.js
  49. 12 1
      packages/ckeditor5-engine/tests/treeview/containerelement.js
  50. 233 0
      packages/ckeditor5-engine/tests/treeview/document/document.js
  51. 12 12
      packages/ckeditor5-engine/tests/treeview/document/integration.js
  52. 0 0
      packages/ckeditor5-engine/tests/treeview/document/jumpoverinlinefiller.html
  53. 101 0
      packages/ckeditor5-engine/tests/treeview/document/jumpoverinlinefiller.js
  54. 0 586
      packages/ckeditor5-engine/tests/treeview/domconverter.js
  55. 327 0
      packages/ckeditor5-engine/tests/treeview/domconverter/binding.js
  56. 432 0
      packages/ckeditor5-engine/tests/treeview/domconverter/dom-to-view.js
  57. 31 0
      packages/ckeditor5-engine/tests/treeview/domconverter/domconverter.js
  58. 321 0
      packages/ckeditor5-engine/tests/treeview/domconverter/view-to-dom.js
  59. 134 0
      packages/ckeditor5-engine/tests/treeview/filler.js
  60. 0 0
      packages/ckeditor5-engine/tests/treeview/manual/__template__.html
  61. 11 11
      packages/ckeditor5-engine/tests/treeview/manual/clickobserver.js
  62. 7 7
      packages/ckeditor5-engine/tests/treeview/manual/focusobserver.js
  63. 27 0
      packages/ckeditor5-engine/tests/treeview/manual/immutable.js
  64. 4 0
      packages/ckeditor5-engine/tests/treeview/manual/immutable.md
  65. 0 4
      packages/ckeditor5-engine/tests/treeview/manual/init.html
  66. 0 24
      packages/ckeditor5-engine/tests/treeview/manual/init.js
  67. 0 13
      packages/ckeditor5-engine/tests/treeview/manual/init.md
  68. 28 0
      packages/ckeditor5-engine/tests/treeview/manual/inline-filler.js
  69. 8 0
      packages/ckeditor5-engine/tests/treeview/manual/inline-filler.md
  70. 5 5
      packages/ckeditor5-engine/tests/treeview/manual/keyobserver.js
  71. 10 12
      packages/ckeditor5-engine/tests/treeview/manual/mutationobserver.js
  72. 30 0
      packages/ckeditor5-engine/tests/treeview/manual/selectionobserver.js
  73. 10 0
      packages/ckeditor5-engine/tests/treeview/manual/selectionobserver.md
  74. 0 35
      packages/ckeditor5-engine/tests/treeview/manual/typing.js
  75. 0 8
      packages/ckeditor5-engine/tests/treeview/manual/typing.md
  76. 33 0
      packages/ckeditor5-engine/tests/treeview/manual/x-index.js
  77. 13 0
      packages/ckeditor5-engine/tests/treeview/manual/x-index.md
  78. 10 10
      packages/ckeditor5-engine/tests/treeview/node.js
  79. 2 0
      packages/ckeditor5-engine/tests/treeview/observer/__template__.html
  80. 10 10
      packages/ckeditor5-engine/tests/treeview/observer/domeventdata.js
  81. 25 28
      packages/ckeditor5-engine/tests/treeview/observer/domeventobserver.js
  82. 7 9
      packages/ckeditor5-engine/tests/treeview/observer/focusobserver.js
  83. 8 10
      packages/ckeditor5-engine/tests/treeview/observer/keyobserver.js
  84. 0 2
      packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.html
  85. 96 35
      packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.js
  86. 3 3
      packages/ckeditor5-engine/tests/treeview/observer/observer.js
  87. 147 0
      packages/ckeditor5-engine/tests/treeview/observer/selectionobserver.js
  88. 37 1
      packages/ckeditor5-engine/tests/treeview/position.js
  89. 642 83
      packages/ckeditor5-engine/tests/treeview/renderer.js
  90. 108 8
      packages/ckeditor5-engine/tests/treeview/selection.js
  91. 0 137
      packages/ckeditor5-engine/tests/treeview/treeview.js
  92. 23 19
      packages/ckeditor5-engine/tests/treeview/writer/breakattributes.js
  93. 23 16
      packages/ckeditor5-engine/tests/treeview/writer/breakrange.js
  94. 1 1
      packages/ckeditor5-engine/tests/treeview/writer/insert.js
  95. 1 1
      packages/ckeditor5-engine/tests/treeview/writer/mergeattributes.js
  96. 1 1
      packages/ckeditor5-engine/tests/treeview/writer/remove.js
  97. 1 1
      packages/ckeditor5-engine/tests/treeview/writer/unwrap.js
  98. 1 1
      packages/ckeditor5-engine/tests/treeview/writer/wrap.js
  99. 2 2
      packages/ckeditor5-engine/tests/treeview/writer/wrapposition.js
  100. 1 1
      packages/ckeditor5-engine/tests/treeview/writer/writer.js

+ 0 - 0
packages/ckeditor5-engine/src/dataprocessor.jsdoc → packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc


+ 0 - 0
packages/ckeditor5-engine/src/core.jsdoc → packages/ckeditor5-engine/src/engine.jsdoc


+ 2 - 2
packages/ckeditor5-engine/src/treecontroller/editingcontroller.js

@@ -5,12 +5,12 @@
 
 'use strict';
 
-import TreeView from '../treeview/treeview.js';
+import ViewDocument from '../treeview/document.js';
 
 export default class EditingController {
 	constructor( modelDocument ) {
 		this.model = modelDocument;
-		this.view = new TreeView();
+		this.view = new ViewDocument();
 	}
 
 	destroy() {}

+ 30 - 2
packages/ckeditor5-engine/src/treecontroller/mapper.js

@@ -12,7 +12,7 @@ import ViewRange from '../treeview/range.js';
 import ViewText from '../treeview/text.js';
 
 /**
- * Maps elements and positions between {@link engine.treeView.TreeView TreeView} and {@link engine.treeModel TreeModel}.
+ * Maps elements and positions between {@link engine.treeView.Document TreeView} and {@link engine.treeModel TreeModel}.
  *
  * Mapper use binded elements to find corresponding elements and positions, so, to get proper results,
  * all Tree Model elements should be {@link engine.treeController.Mapper#bindElements binded}.
@@ -254,7 +254,7 @@ export default class Mapper {
 
 		// If it equals we found the position.
 		if ( modelOffset == expectedOffset ) {
-			return new ViewPosition( viewParent, viewOffset );
+			return this._moveViewPositionToTextNode( new ViewPosition( viewParent, viewOffset ) );
 		}
 		// If it is higher we need to enter last child.
 		else {
@@ -263,4 +263,32 @@ export default class Mapper {
 			return this._findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) );
 		}
 	}
+
+	/**
+	 * Because we prefer positions in text nodes over positions next to text node moves view position to the text node
+	 * if it was next to it.
+	 *
+	 *		<p>[]<b>foo</b></p> -> <p>[]<b>foo</b></p> // do not touch if position is not directly next to text
+	 *		<p>foo[]<b>foo</b></p> -> <p>foo{}<b>foo</b></p> // move to text node
+	 *		<p><b>[]foo</b></p> -> <p><b>{}foo</b></p> // move to text node
+	 *
+	 * @private
+	 * @param {engine.treeView.Position} viewPosition Position potentially next to text node.
+	 * @returns {engine.treeView.Position} Position in text node if possible.
+	 */
+	_moveViewPositionToTextNode( viewPosition ) {
+		// If the position is just after text node, put it at the end of that text node.
+		// If the position is just before text node, put it at the beginning of that text node.
+		const nodeBefore = viewPosition.nodeBefore;
+		const nodeAfter = viewPosition.nodeAfter;
+
+		if ( nodeBefore instanceof ViewText ) {
+			return new ViewPosition( nodeBefore, nodeBefore.data.length );
+		} else if ( nodeAfter instanceof ViewText ) {
+			return new ViewPosition( nodeAfter, 0 );
+		}
+
+		// Otherwise, just return the given position.
+		return viewPosition;
+	}
 }

+ 11 - 6
packages/ckeditor5-engine/src/treecontroller/model-converter-builder.js

@@ -11,10 +11,12 @@ import {
 	removeAttribute,
 	wrap,
 	unwrap
-} from '/ckeditor5/engine/treecontroller/model-to-view-converters.js';
+} from './model-to-view-converters.js';
 
-import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
-import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
+import { convertSelectionAttribute } from './model-selection-to-view-converters.js';
+
+import ViewAttributeElement from '../treeview/attributeelement.js';
+import ViewContainerElement from '../treeview/containerelement.js';
 
 /**
  * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
@@ -165,9 +167,10 @@ class ModelConverterBuilder {
 	 * Creator function will be passed different values depending whether conversion is from element or from attribute:
 	 *
 	 * * from element: dispatcher's {@link engine.treeController.ModelConversionDispatcher#event:insert insert event} parameters
-	 * will be passed
-	 * * from attribute: first parameter is attribute value, then the rest of parameters are dispatcher's
-	 * {@link engine.treeController.ModelConversionDispatcher#event:changeAttribute changeAttribute event} parameters.
+	 * will be passed,
+	 * * from attribute: there is one parameter and it is attribute value.
+	 *
+	 * This method also registers model selection to view selection converter, if conversion is from attribute.
 	 *
 	 * This method creates the converter and adds it as a callback to a proper
 	 * {@link engine.treeController.ModelConversionDispatcher conversion dispatcher} event.
@@ -190,6 +193,8 @@ class ModelConverterBuilder {
 				dispatcher.on( 'addAttribute:' + this._from.key, wrap( element ), null, priority );
 				dispatcher.on( 'changeAttribute:' + this._from.key, wrap( element ), null, priority );
 				dispatcher.on( 'removeAttribute:' + this._from.key, unwrap( element ), null, priority );
+
+				dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), null, priority );
 			}
 		}
 	}

+ 210 - 0
packages/ckeditor5-engine/src/treecontroller/model-selection-to-view-converters.js

@@ -0,0 +1,210 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ViewElement from '../treeview/element.js';
+import ViewRange from '../treeview/range.js';
+
+/**
+ * Contains {@link engine.treeModel.Selection model selection} to {@link engine.treeView.Selection view selection} converters for
+ * {@link engine.treeController.ModelConversionDispatcher}.
+ *
+ * @namespace engine.treeController.modelSelectionToView
+ */
+
+/**
+ * Function factory, creates a converter that converts non-collapsed {@link engine.treeModel.Selection model selection} to
+ * {@link engine.treeView.Selection view selection}. The converter consumes appropriate value from `consumable` object
+ * and maps model positions from selection to view positions.
+ *
+ *		modelDispatcher.on( 'selection', convertRangeSelection() );
+ *
+ * @external engine.treeController.modelSelectionToView
+ * @function engine.treeController.modelSelectionToView.convertRangeSelection
+ * @returns {Function} Selection converter.
+ */
+export function convertRangeSelection() {
+	return ( evt, data, consumable, conversionApi ) => {
+		const selection = data.selection;
+
+		if ( selection.isCollapsed ) {
+			return;
+		}
+
+		if ( !consumable.consume( selection, 'selection' ) ) {
+			return;
+		}
+
+		conversionApi.viewSelection.removeAllRanges();
+
+		for ( let range of selection.getRanges() ) {
+			const viewRange = conversionApi.mapper.toViewRange( range );
+			conversionApi.viewSelection.addRange( viewRange, selection.isBackward );
+		}
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts collapsed {@link engine.treeModel.Selection model selection} to
+ * {@link engine.treeView.Selection view selection}. The converter consumes appropriate value from `consumable` object,
+ * maps model selection position to view position and breaks {@link engine.treeView.AttributeElement attribute elements}
+ * at the selection position.
+ *
+ *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
+ *
+ * Example of view state before and after converting collapsed selection:
+ *
+ *		<p><strong>f^oo<strong>bar</p>
+ *		-> <p><strong>f</strong>^<strong>oo</strong>bar</p>
+ *
+ * By breaking attribute elements like `<strong>` selection is in correct elements. See also complementary
+ * {@link engine.treeController.modelSelectionToView.convertSelectionAttribute attribute converter} for selection attributes,
+ * which wraps collapsed selection into view elements. Those converters together ensure, that selection ends up in
+ * appropriate elements.
+ *
+ * See also {@link engine.treeController.modelSelectionToView.clearAttributes} which do the clean-up by merging attributes.
+ *
+ * @external engine.treeController.modelSelectionToView
+ * @function engine.treeController.modelSelectionToView.convertCollapsedSelection
+ * @returns {Function} Selection converter.
+ */
+export function convertCollapsedSelection() {
+	return ( evt, data, consumable, conversionApi ) => {
+		const selection = data.selection;
+
+		if ( !selection.isCollapsed ) {
+			return;
+		}
+
+		if ( !consumable.consume( selection, 'selection' ) ) {
+			return;
+		}
+
+		const modelPosition = selection.getFirstPosition();
+		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
+		const brokenPosition = conversionApi.writer.breakAttributes( viewPosition );
+
+		conversionApi.viewSelection.removeAllRanges();
+		conversionApi.viewSelection.addRange( new ViewRange( brokenPosition, brokenPosition ) );
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts {@link engine.treeModel.Selection model selection} attributes to
+ * {@link engine.treeView.AttributeElement view attribute elements}. The converter works only for collapsed selection.
+ * The converter consumes appropriate value from `consumable` object, maps model selection position to view position and
+ * wraps that position into a view attribute element.
+ *
+ * The wrapping node depends on passed parameter. If {@link engine.treeView.Element} was passed, it will be cloned and
+ * the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
+ * {@link engine.treeController.ModelConversionDispatcher#event:selectionAttribute selectionAttribute event}. It's expected that
+ * the function returns a {@link engine.treeView.AttributeElement}. The result of the function will be the wrapping element.
+ *
+ *		modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
+ *
+ *		function styleElementCreator( styleValue ) {
+ *			if ( styleValue == 'important' ) {
+ *				return new ViewAttributeElement( 'strong', { style: 'text-transform:uppercase;' } );
+ *			} else if ( styleValue == 'gold' ) {
+ *				return new ViewAttributeElement( 'span', { style: 'color:yellow;' } );
+ *			}
+ *		}
+ *		modelDispatcher.on( 'selectionAttribute:style', convertSelectionAttribute( styleCreator ) );
+ *
+ * **Note:** You can use the same `elementCreator` function for this converter factory and {@link engine.treeController.modelToView.wrap}
+ * model to view converter, as long as the `elementCreator` function uses only the first parameter (attribute value).
+ *
+ * Example of view state after converting collapsed selection. The scenario is: selection is inside bold text (`<strong>` element)
+ * but it does not have bold attribute itself, but has italic attribute instead (let's assume that user turned off bold and turned
+ * on italic with selection collapsed):
+ *
+ *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
+ *		modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
+ *
+ * Example of view states before and after converting collapsed selection:
+ *
+ *		<p><em>f^oo</em>bar</p>
+ *		-> <p><em>f</em>^<em>oo</em>bar</p>
+ *		-> <p><em>f^oo</em>bar</p>
+ *
+ *		<p><strong>f^oo<strong>bar</p>
+ *		-> <p><strong>f</strong>^<strong>oo<strong>bar</p>
+ *		-> <p><strong>f</strong><em>^</em><strong>oo</strong>bar</p>
+ *
+ * In first example, nothing has changed, because first `<em>` element got broken by `convertCollapsedSelection()` converter,
+ * but then it got wrapped-back by `convertSelectionAttribute()` converter. In second example, notice how `<strong>` element
+ * is broken to prevent putting selection in it, since selection has no `bold` attribute.
+ *
+ * @external engine.treeController.modelSelectionToView
+ * @function engine.treeController.modelSelectionToView.convertCollapsedSelection
+ * @param {engine.treeView.AttributeElement|Function} elementCreator View element, or function returning a view element, which will
+ * be used for wrapping.
+ * @returns {Function} Selection converter.
+ */
+export function convertSelectionAttribute( elementCreator ) {
+	return ( evt, data, consumable, conversionApi ) => {
+		const selection = data.selection;
+
+		if ( !selection.isCollapsed ) {
+			return;
+		}
+
+		if ( !consumable.consume( selection, 'selectionAttribute:' + data.key ) ) {
+			return;
+		}
+
+		let viewPosition = conversionApi.viewSelection.getFirstPosition();
+		conversionApi.viewSelection.removeAllRanges();
+
+		const viewElement = elementCreator instanceof ViewElement ?
+				elementCreator.clone( true ) :
+				elementCreator( data.value, selection, consumable, conversionApi );
+
+		viewPosition = conversionApi.writer.wrapPosition( viewPosition, viewElement );
+
+		conversionApi.viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
+	};
+}
+
+/**
+ * Function factory, creates a converter that clears artifacts after the previous
+ * {@link engine.treeModel.Selection model selection} conversion. It removes all empty
+ * {@link engine.treeView.AttributeElement view attribute elements} and merge sibling attributes at all start and end
+ * positions of all ranges.
+ *
+ *		<p><strong>^</strong></p>
+ *		-> <p>^</p>
+ *
+ *		<p><strong>foo</strong>^<strong>bar</strong>bar</p>
+ *		-> <p><strong>foo^bar<strong>bar</p>
+ *
+ *		<p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
+ *		-> <p><strong>foo^bar<strong>bar</p>
+ *
+ * This listener should be assigned before any converter for the new selection:
+ *
+ *		modelDispatcher.on( 'selection', clearAttributes() );
+ *
+ * See {@link engine.treeController.modelSelectionToView.convertCollapsedSelection} which do the opposite by breaking
+ * attributes in the selection position.
+ *
+ * @external engine.treeController.modelSelectionToView
+ * @function engine.treeController.modelSelectionToView.clearAttributes
+ * @returns {Function} Selection converter.
+ */
+export function clearAttributes() {
+	return ( evt, data, consumable, conversionApi ) => {
+		for ( let range of conversionApi.viewSelection.getRanges() ) {
+			const parentNode = range.start.parent;
+
+			if ( parentNode instanceof ViewElement && parentNode.getChildCount() === 0 ) {
+				parentNode.parent.removeChildren( parentNode.getIndex() );
+			}
+		}
+
+		conversionApi.viewSelection.removeAllRanges();
+	};
+}

+ 21 - 14
packages/ckeditor5-engine/src/treecontroller/modelconsumable.js

@@ -10,8 +10,8 @@ import TextProxy from '../treemodel/textproxy.js';
 /**
  * Manages a list of consumable values for {@link engine.treeModel.Item model items}.
  *
- * Consumables are various aspects of the model items. A model item can be broken down into singular properties
- * that might be taken into consideration when converting that item.
+ * Consumables are various aspects of the model. A model item can be broken down into singular properties that might be
+ * taken into consideration when converting that item.
  *
  * `ModelConsumable` is used by {@link engine.treeController.ModelConversionDispatcher} while analyzing changed
  * parts of {@link engine.treeModel.Document the document}. The added / changed / removed model items are broken down
@@ -19,14 +19,13 @@ import TextProxy from '../treemodel/textproxy.js';
  * during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
  * but without attributes), consumable value is removed from `ModelConsumable`.
  *
- * Although `ModelConsumable` might store consumable values of any type (see {@link engine.treeController.ModelConsumable#add}),
- * the commonly used ones are similar to events names fired by {@link engine.treeController.ModelConversionDispatcher}:
- * `insert`, `addAttribute:<attributeKey>`, `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`.
+ * For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addAttribute:<attributeKey>`,
+ * `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`.
  *
- * Most commonly, {@link engine.treeController.ModelConsumable#add add method} will be used only by
- * {@link engine.treeController.ModelConversionDispatcher} to gather consumable values. However, it is important to
- * understand how consumable values can be {@link engine.treeController.ModelConsumable#consume consumed}. See also:
- * {@link engine.treeController.modelToView}.
+ * In most cases, it is enough to let {@link engine.treeController.ModelConversionDispatcher} gather consumable values, so
+ * there is no need to use {@link engine.treeController.ModelConsumable#add add method} directly. However, it is important to
+ * understand how consumable values can be {@link engine.treeController.ModelConsumable#consume consumed}. See
+ * {@link engine.treeController.modelToView default model to view converters} for more information.
  *
  * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
  * able to convert one or more parts of the model. However, when one of those callbacks actually converts
@@ -109,7 +108,7 @@ export default class ModelConsumable {
 		 * `ModelConsumable` translates `TextProxy` to that unique `Symbol`.
 		 *
 		 * @private
-		 * @member {Map} engine.treeController.ModelConsumable#_textParentsRegistry
+		 * @member {Map} engine.treeController.ModelConsumable#_textProxyRegistry
 		 */
 		this._textProxyRegistry = new Map();
 	}
@@ -120,8 +119,10 @@ export default class ModelConsumable {
 	 *		modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values.
 	 *		modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
 	 *		modelConsumable.add( modelElement, 'removeAttribute:bold' ); // Add `bold` attribute removal on `modelElement` change.
+	 *		modelConsumable.add( modelSelection, 'selection' ); // Add `modelSelection` to consumable values.
+	 *		modelConsumable.add( modelSelection, 'selectionAttribute:bold' ); // Add `bold` attribute on `modelSelection` to consumables.
 	 *
-	 * @param {engine.treeModel.Item} item Model item that has the consumable.
+	 * @param {engine.treeModel.Item|engine.treeModel.Selection} item Model item or selection that has the consumable.
 	 * @param {String} type Consumable type.
 	 */
 	add( item, type ) {
@@ -142,8 +143,10 @@ export default class ModelConsumable {
 	 *		modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values.
 	 *		modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
 	 *		modelConsumable.consume( modelElement, 'removeAttribute:bold' ); // Remove `bold` attribute removal on `modelElement` change.
+	 *		modelConsumable.consume( modelSelection, 'selection' ); // Remove `modelSelection` from consumable values.
+	 *		modelConsumable.consume( modelSelection, 'selectionAttribute:bold' ); // Remove `bold` on `modelSelection` from consumables.
 	 *
-	 * @param {engine.treeModel.Item} item Model item from which consumable will be consumed.
+	 * @param {engine.treeModel.Item|engine.treeModel.Selection} item Model item or selection from which consumable will be consumed.
 	 * @param {String} type Consumable type.
 	 * @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise.
 	 */
@@ -167,8 +170,10 @@ export default class ModelConsumable {
 	 *		modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change.
 	 *		modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
 	 *		modelConsumable.test( modelElement, 'removeAttribute:bold' ); // Check for `bold` attribute removal on `modelElement` change.
+	 *		modelConsumable.test( modelSelection, 'selection' ); // Check if `modelSelection` is consumable.
+	 *		modelConsumable.test( modelSelection, 'selectionAttribute:bold' ); // Check if `bold` on `modelSelection` is consumable.
 	 *
-	 * @param {engine.treeModel.Item} item Model item that will be tested.
+	 * @param {engine.treeModel.Item|engine.treeModel.Selection} item Model item or selection that will be tested.
 	 * @param {String} type Consumable type.
 	 * @returns {null|Boolean} `null` if such consumable was never added, `false` if the consumable values was
 	 * already consumed or `true` if it was added and not consumed yet.
@@ -199,8 +204,10 @@ export default class ModelConsumable {
 	 *		modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change.
 	 *		modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
 	 *		modelConsumable.revert( modelElement, 'removeAttribute:bold' ); // Revert consuming `bold` attribute remove from `modelElement`.
+	 *		modelConsumable.revert( modelSelection, 'selection' ); // Revert consuming `modelSelection`.
+	 *		modelConsumable.revert( modelSelection, 'selectionAttribute:bold' ); // Revert consuming `bold` from `modelSelection`.
 	 *
-	 * @param {engine.treeModel.Item} item Model item that will be tested.
+	 * @param {engine.treeModel.Item|engine.treeModel.Selection} item Model item or selection that will be reverted.
 	 * @param {String} type Consumable type.
 	 * @returns {null|Boolean} `true` if consumable has been reversed, `false` otherwise. `null` if the consumable has
 	 * never been added.

+ 70 - 0
packages/ckeditor5-engine/src/treecontroller/modelconversiondispatcher.js

@@ -247,6 +247,32 @@ export default class ModelConversionDispatcher {
 		}
 	}
 
+	/**
+	 * Fires events for given {@link engine.treeModel.Selection selection} to start selection conversion.
+	 *
+	 * @fires engine.treeController.ModelConversionDispatcher#selection
+	 * @fires engine.treeController.ModelConversionDispatcher#selectionAttribute
+	 * @param {engine.treeModel.Selection} selection Selection to convert.
+	 */
+	convertSelection( selection ) {
+		const consumable = this._createSelectionConsumable( selection );
+		const data = {
+			selection: selection
+		};
+
+		this.fire( 'selection', data, consumable, this.conversionApi );
+
+		for ( let attr of selection.getAttributes() ) {
+			data.key = attr[ 0 ];
+			data.value = attr[ 1 ];
+
+			// Do not fire event if the attribute has been consumed.
+			if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
+				this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
+			}
+		}
+	}
+
 	/**
 	 * Creates {@link engine.treeController.ModelConsumable} with values to consume from given range, assuming that
 	 * given range has just been inserted to the model.
@@ -293,6 +319,25 @@ export default class ModelConversionDispatcher {
 		return consumable;
 	}
 
+	/**
+	 * Creates {@link engine.treeController.ModelConsumable} with selection consumable values.
+	 *
+	 * @private
+	 * @param {engine.treeModel.Selection} selection Selection to create consumable from.
+	 * @returns {engine.treeController.ModelConsumable} Values to consume.
+	 */
+	_createSelectionConsumable( selection ) {
+		const consumable = new Consumable();
+
+		consumable.add( selection, 'selection' );
+
+		for ( let attr of selection.getAttributes() ) {
+			consumable.add( selection, 'selectionAttribute:' + attr[ 0 ] );
+		}
+
+		return consumable;
+	}
+
 	/**
 	 * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
 	 *
@@ -422,6 +467,31 @@ export default class ModelConversionDispatcher {
 	 * @param {engine.treeController.ModelConsumable} consumable Values to consume.
 	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
 	 */
+
+	/**
+	 * Fired for {@link engine.treeModel.Selection selection} changes.
+	 *
+	 * @event engine.treeController.ModelConversionDispatcher.selection
+	 * @param {engine.treeModel.Selection} selection `Selection` instance that is converted.
+	 * @param {engine.treeController.ModelConsumable} consumable Values to consume.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 */
+
+	/**
+	 * Fired for {@link engine.treeModel.Selection selection} attributes changes.
+	 *
+	 * `selectionAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `selectionAttribute:<attributeKey>`. `attributeKey` is the key of selection attribute. This way listen can listen to
+	 * certain attribute, i.e. `addAttribute:bold`.
+	 *
+	 * @event engine.treeController.ModelConversionDispatcher.selectionAttribute
+	 * @param {Object} data Additional information about the change.
+	 * @param {engine.treeModel.Selection} data.selection Selection that is converted.
+	 * @param {String} data.attributeKey Key of changed attribute.
+	 * @param {*} data.attributeValue Value of changed attribute.
+	 * @param {engine.treeController.ModelConsumable} consumable Values to consume.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
+	 */
 }
 
 mix( ModelConversionDispatcher, EmitterMixin );

+ 0 - 0
packages/ckeditor5-engine/src/treecontroller.jsdoc → packages/ckeditor5-engine/src/treecontroller/treecontroller.jsdoc


+ 1 - 1
packages/ckeditor5-engine/src/treemodel/operation/moveoperation.js

@@ -139,7 +139,7 @@ export default class MoveOperation extends Operation {
 			throw new CKEditorError(
 				'operation-move-range-into-itself: Trying to move a range of nodes to the inside of that range.'
 			);
-		} else {
+		} else if ( this.sourcePosition.root == this.targetPosition.root ) {
 			if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'PREFIX' ) {
 				let i = this.sourcePosition.path.length - 1;
 

+ 23 - 11
packages/ckeditor5-engine/src/treemodel/selection.js

@@ -93,18 +93,31 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns whether the selection is collapsed. Selection is collapsed when all it's ranges are collapsed.
+	 * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
+	 * collapsed.
 	 *
 	 * @type {Boolean}
 	 */
 	get isCollapsed() {
-		for ( let i = 0; i < this._ranges.length; i++ ) {
-			if ( !this._ranges[ i ].isCollapsed ) {
-				return false;
-			}
+		const length = this._ranges.length;
+
+		if ( length === 0 ) {
+			// Default range is collapsed.
+			return true;
+		} else if ( length === 1 ) {
+			return this._ranges[ 0 ].isCollapsed;
+		} else {
+			return false;
 		}
+	}
 
-		return true;
+	/**
+	 * Returns number of ranges in selection.
+	 *
+	 * @type {Number}
+     */
+	get rangeCount() {
+		return this._ranges.length ? this._ranges.length : 1;
 	}
 
 	/**
@@ -469,7 +482,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Updates this selection attributes based on it's position in the Tree Model.
+	 * Updates this selection attributes based on it's position in the model.
 	 *
 	 * @protected
 	 */
@@ -479,7 +492,7 @@ export default class Selection {
 
 		let attrs = null;
 
-		if ( this.isCollapsed === false ) {
+		if ( !this.isCollapsed ) {
 			// 1. If selection is a range...
 			const range = this.getFirstRange();
 
@@ -490,10 +503,9 @@ export default class Selection {
 					break;
 				}
 			}
-		}
+		} else {
+			// 2. If the selection is a caret or the range does not contain a character node...
 
-		// 2. If the selection is a caret or the range does not contain a character node...
-		if ( !attrs && this.isCollapsed === true ) {
 			const nodeBefore = positionParent.getChild( position.offset - 1 );
 			const nodeAfter = positionParent.getChild( position.offset );
 

+ 0 - 0
packages/ckeditor5-engine/src/treemodel.jsdoc → packages/ckeditor5-engine/src/treemodel/treemodel.jsdoc


+ 43 - 10
packages/ckeditor5-engine/src/treeview/attributeelement.js

@@ -6,13 +6,10 @@
 'use strict';
 
 import Element from './element.js';
+import ContainerElement from './containerelement.js';
 
-/**
- * Default attribute priority.
- *
- * @member {Number} engine.treeView.AttributeElement.DEFAULT_PRIORITY
- */
-export const DEFAULT_PRIORITY = 10;
+// Default attribute priority.
+const DEFAULT_PRIORITY = 10;
 
 /**
  * Attributes are elements which define document presentation. They are mostly elements like `<b>` or `<span>`.
@@ -37,7 +34,7 @@ export default class AttributeElement extends Element {
 		/**
 		 * Element priority. Attributes have to have the same priority to be
 		 * {@link engine.treeView.Element#isSimilar similar}. Setting different priorities on similar
- 		 * nodes may prevent merging, eg. 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.
 		 *
 		 * @member {Number} engine.treeView.AttributeElement#priority
 		 */
@@ -49,13 +46,13 @@ export default class AttributeElement extends Element {
 	 *
 	 * @param {Boolean} deep If set to `true` clones element and all its children recursively. When set to `false`,
 	 * element will be cloned without any children.
-	 * @returns {Element} Clone of this element.
+	 * @returns {engine.treeView.AttributeElement} Clone of this element.
 	 */
 	clone( deep ) {
 		const cloned = super.clone( deep );
 
 		// Clone priority too.
-		cloned.priority	= this.priority;
+		cloned.priority = this.priority;
 
 		return cloned;
 	}
@@ -65,10 +62,46 @@ export default class AttributeElement extends Element {
 	 * Both elements should have the same name, attributes and priority to be considered as similar.
 	 * Two similar elements can contain different set of children nodes.
 	 *
-	 * @param {Element} otherElement
+	 * @param {engine.treeView.Element} otherElement
 	 * @returns {Boolean}
 	 */
 	isSimilar( otherElement ) {
 		return super.isSimilar( otherElement ) && this.priority == otherElement.priority;
 	}
+
+	/**
+	 * Returns block {@link engine.treeView.filler filler} offset or `null` if a block filler is not needed.
+	 *
+	 * @returns {Number|null} Block filler offset or `null` if block filler is not needed.
+	 */
+	getFillerOffset() {
+		// <b>foo</b> does not need filler.
+		if ( this.getChildCount() ) {
+			return null;
+		}
+
+		let element = this.parent;
+
+		// <p><b></b></p> needs filler -> <p><b><br></b></p>
+		while ( !( element instanceof ContainerElement ) ) {
+			if ( element.getChildCount() > 1 ) {
+				return null;
+			}
+
+			element = element.parent;
+		}
+
+		if ( element.getChildCount() > 1 ) {
+			return null;
+		}
+
+		return 0;
+	}
 }
+
+/**
+ * Default attribute priority.
+ *
+ * @member {Number} engine.treeView.AttributeElement.DEFAULT_PRIORITY
+ */
+AttributeElement.DEFAULT_PRIORITY = DEFAULT_PRIORITY;

+ 14 - 5
packages/ckeditor5-engine/src/treeview/containerelement.js

@@ -21,9 +21,9 @@ import Element from './element.js';
  * DOM properly. {@link engine.treeView.DomConverter} will ensure that `ContainerElement` is editable and it is possible
  * to put caret inside it, even if the container is empty.
  *
- * Secondly, {@link engine.treeView.Writer} use this information.
- * {@link engine.treeView.Writer#breakAttributes Break} and {@link engine.treeView.Writer#mergeAttributes Merge}
- * operations are performed only in a bounds of a container nodes.
+ * Secondly, {@link engine.treeView.Writer} uses this information.
+ * Nodes {@link engine.treeView.Writer#breakAttributes breaking} and {@link engine.treeView.Writer#mergeAttributes merging}
+ * is performed only in a bounds of a container nodes.
  *
  * For instance if `<p>` is an container and `<b>` is attribute:
  *
@@ -36,8 +36,8 @@ import Element from './element.js';
  * There might be a need to mark `<span>` element as a container node, for example in situation when it will be a
  * container of an inline widget:
  *
- * 		<span color="red">foobar</span>  // attribute
- * 		<span data-widget>foobar</span>  // container
+ *		<span color="red">foobar</span>		// attribute
+ *		<span data-widget>foobar</span>		// container
  *
  * @memberOf engine.treeView
  * @extends engine.treeView.Element
@@ -51,4 +51,13 @@ export default class ContainerElement extends Element {
 	constructor( name, attrs, children ) {
 		super( name, attrs, children );
 	}
+
+	/**
+	 * Returns block {@link engine.treeView.filler filler} offset or `null` if block filler is not needed.
+	 *
+	 * @returns {Number|null} Block filler offset or `null` if block filler is not needed.
+	 */
+	getFillerOffset() {
+		return this.getChildCount() === 0 ? 0 : null;
+	}
 }

+ 70 - 42
packages/ckeditor5-engine/src/treeview/treeview.js → packages/ckeditor5-engine/src/treeview/document.js

@@ -5,15 +5,17 @@
 
 'use strict';
 
-import EmitterMixin from '../../utils/emittermixin.js';
+import Selection from './selection.js';
 import Renderer from './renderer.js';
-import DomConverter from './domconverter.js';
 import Writer from './writer.js';
+import DomConverter from './domconverter.js';
+import { injectQuirksHandling } from './filler.js';
 
 import mix from '../../utils/mix.js';
+import EmitterMixin from '../../utils/emittermixin.js';
 
 /**
- * TreeView class creates an abstract layer over the content editable area.
+ * Document class creates an abstract layer over the content editable area.
  * It combines the actual tree of view elements, tree of DOM elements,
  * {@link engine.treeView.DomConverter DOM Converter}, {@link engine.treeView.Renderer renderer} and all
  * {@link engine.treeView.Observer observers}.
@@ -24,33 +26,41 @@ import mix from '../../utils/mix.js';
  * @memberOf engine.treeView
  * @mixes utils.EmitterMixin
  */
-export default class TreeView {
+export default class Document {
 	/**
-	 * Creates a TreeView instance.
+	 * Creates a Document instance.
 	 */
 	constructor() {
 		/**
 		 * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
 		 *
 		 * @readonly
-		 * @member {Map} engine.treeView.TreeView#domRoots
+		 * @member {Map} engine.treeView.Document#domRoots
 		 */
 		this.domRoots = new Map();
 
+		/**
+		 * Selection done on this document.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.Selection} engine.treeView.Document#selection
+		 */
+		this.selection = new Selection();
+
 		/**
 		 * Tree View writer.
 		 *
 		 * @readonly
-		 * @member {engine.treeView.Writer} engine.treeView.TreeView#writer
+		 * @member {engine.treeView.Writer} engine.treeView.Document#writer
 		 */
 		this.writer = new Writer();
 
 		/**
 		 * Instance of the {@link engine.treeView.DomConverter domConverter} use by
-		 * {@link engine.treeView.TreeView#renderer renderer} and {@link engine.treeView.observer.Observer observers}.
+		 * {@link engine.treeView.Document#renderer renderer} and {@link engine.treeView.observer.Observer observers}.
 		 *
 		 * @readonly
-		 * @member {engine.treeView.DomConverter} engine.treeView.TreeView#domConverter
+		 * @member {engine.treeView.DomConverter} engine.treeView.Document#domConverter
 		 */
 		this.domConverter = new DomConverter();
 
@@ -58,31 +68,33 @@ export default class TreeView {
 		 * Roots of the view tree. Map of the {engine.treeView.Element view elements} with roots names as keys.
 		 *
 		 * @readonly
-		 * @member {Map} engine.treeView.TreeView#viewRoots
+		 * @member {Map} engine.treeView.Document#viewRoots
 		 */
 		this.viewRoots = new Map();
 
 		/**
-		 * Instance of the {@link engine.treeView.TreeView#renderer renderer}.
+		 * Instance of the {@link engine.treeView.Document#renderer renderer}.
 		 *
 		 * @readonly
-		 * @member {engine.treeView.Renderer} engine.treeView.TreeView#renderer
+		 * @member {engine.treeView.Renderer} engine.treeView.Document#renderer
 		 */
-		this.renderer = new Renderer( this.domConverter );
+		this.renderer = new Renderer( this.domConverter, this.selection );
 
 		/**
-		 * Set of registered {@link engine.treeView.Observer observers}.
+		 * Map of registered {@link engine.treeView.Observer observers}.
 		 *
-		 * @protected
-		 * @member {Set.<engine.treeView.Observer>} engine.treeView.TreeView_#observers
+		 * @private
+		 * @member {Map.<Function, engine.treeView.Observer>} engine.treeView.Document_#observers
 		 */
-		this._observers = new Set();
+		this._observers = new Map();
+
+		injectQuirksHandling( this );
 	}
 
 	/**
 	 * Creates observer of the given type if not yet created, {@link engine.treeView.Observer#enable enables} it
 	 * and {@link engine.treeView.observer.Observer#observe attaches} to all existing and future
-	 * {@link engine.treeView.TreeView#domRoots DOM roots}.
+	 * {@link engine.treeView.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
@@ -90,39 +102,54 @@ export default class TreeView {
 	 *
 	 * @param {Function} Observer The constructor of an observer to add.
 	 * Should create an instance inheriting from {@link engine.treeView.observer.Observer}.
+	 * @returns {engine.treeView.observer.Observer} Added observer instance.
 	 */
 	addObserver( Observer ) {
-		if ( this._hasObserver( Observer ) ) {
-			return;
+		let observer = this._observers.get( Observer );
+
+		if ( observer ) {
+			return observer;
 		}
 
-		const observer = new Observer( this );
+		observer = new Observer( this );
 
-		this._observers.add( observer );
+		this._observers.set( Observer, observer );
 
 		for ( let [ name, domElement ] of this.domRoots ) {
 			observer.observe( domElement, name );
 		}
 
 		observer.enable();
+
+		return observer;
 	}
 
 	/**
-	 * Creates a root for the HTMLElement. It adds elements to {@link engine.treeView.TreeView#domRoots} and
-	 * {@link engine.treeView.TreeView#viewRoots}.
+	 * 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 {engine.treeView.observer.Observer|undefined} Observer instance or undefined.
+	 */
+	getObserver( Observer ) {
+		return this._observers.get( Observer );
+	}
+
+	/**
+	 * Creates a root for the HTMLElement. It adds elements to {@link engine.treeView.Document#domRoots} and
+	 * {@link engine.treeView.Document#viewRoots}.
 	 *
 	 * The constructor copies the element name and attributes to create the
 	 * root of the view, but does not copy its children. This means that while
-	 * {@link engine.treeView.TreeView#render rendering}, the whole content of this
+	 * {@link engine.treeView.Document#render rendering}, the whole content of this
 	 * root element will be removed but the root name and attributes will be preserved.
 	 *
 	 * @param {HTMLElement} domRoot DOM element in which the tree view should do change.
-	 * @param {String} name Name of the root.
+	 * @param {String} [name='main'] Name of the root.
 	 * @returns {engine.treeView.element} The created view root element.
 	 */
-	createRoot( domRoot, name ) {
+	createRoot( domRoot, name = 'main' ) {
 		const viewRoot = this.domConverter.domToView( domRoot, { bind: true, withChildren: false } );
-		viewRoot.setTreeView( this );
+		viewRoot.setDocument( this );
 
 		// Mark changed nodes in the renderer.
 		viewRoot.on( 'change', ( evt, type, node ) => {
@@ -133,41 +160,42 @@ export default class TreeView {
 		this.domRoots.set( name, domRoot );
 		this.viewRoots.set( name, viewRoot );
 
-		for ( let observer of this._observers ) {
+		for ( let observer of this._observers.values() ) {
 			observer.observe( domRoot, name );
 		}
 
 		return viewRoot;
 	}
 
+	/**
+	 * Get a {@link engine.treeView.Document#viewRoots view root element} with the specified name. If the name is not
+	 * specific "main" root is returned.
+	 *
+	 * @param {String} [name='main']  Name of the root.
+	 * @returns {engine.treeView.element} The view root element with the specified name.
+	 */
+	getRoot( name = 'main' ) {
+		return this.viewRoots.get( name );
+	}
+
 	/**
 	 * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers all detached
 	 * before rendering and reattached after that.
 	 */
 	render() {
-		for ( let observer of this._observers ) {
+		for ( let observer of this._observers.values() ) {
 			observer.disable();
 		}
 
 		this.renderer.render();
 
-		for ( let observer of this._observers ) {
+		for ( let observer of this._observers.values() ) {
 			observer.enable();
 		}
 	}
-
-	/**
-	 * Checks whether the given observer was already added.
-	 *
-	 * @private
-	 * @param {Function} Observer The observer constructor to check.
-	 */
-	_hasObserver( Observer ) {
-		return Array.from( this._observers ).some( ( observer ) => observer.constructor === Observer );
-	}
 }
 
-mix( TreeView, EmitterMixin );
+mix( Document, EmitterMixin );
 
 /**
  * Enum representing type of the change.

+ 289 - 57
packages/ckeditor5-engine/src/treeview/domconverter.js

@@ -7,14 +7,20 @@
 
 import ViewText from './text.js';
 import ViewElement from './element.js';
+import ViewPosition from './position.js';
+import ViewRange from './range.js';
+import ViewSelection from './selection.js';
 import ViewDocumentFragment from './documentfragment.js';
+import { BR_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, isInlineFiller, startsWithFiller, getDataWithoutFiller } from './filler.js';
+
+import indexOf from '../../utils/dom/indexof.js';
 
 /**
  * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
  * {@link engine.treeView.DomConverter#bindElements binding} these nodes.
  *
  * DomConverter does not check which nodes should be rendered (use {@link engine.treeView.Renderer}), does not keep a
- * state of a tree nor keeps synchronization between tree view and DOM tree (use {@link engine.treeView.TreeView}).
+ * state of a tree nor keeps synchronization between tree view and DOM tree (use {@link engine.treeView.Document}).
  *
  * DomConverter keeps DOM elements to View element bindings, so when the converter will be destroyed, the binding will
  * be lost. Two converters will keep separate binding maps, so one tree view can be bound with two DOM trees.
@@ -24,8 +30,11 @@ import ViewDocumentFragment from './documentfragment.js';
 export default class DomConverter {
 	/**
 	 * Creates DOM converter.
+	 *
+	 * @param {Object} options Object with configuration options.
+	 * @param {Function} [options.blockFiller=engine.treeView.filler.BR_FILLER] Block filler creator.
 	 */
-	constructor() {
+	constructor( options = {} ) {
 		// Using WeakMap prevent memory leaks: when the converter will be destroyed all referenced between View and DOM
 		// will be removed. Also because it is a *Weak*Map when both view and DOM elements will be removed referenced
 		// will be also removed, isn't it brilliant?
@@ -33,6 +42,17 @@ export default class DomConverter {
 		// Yes, PJ. It is.
 		//
 		// You guys so smart.
+		//
+		// I've been here. Seen stuff. Afraid of code now.
+
+		/**
+		 * Block {@link engine.treeView.filler filler} creator, which is used to create all block fillers during the
+		 * view to DOM conversion and to recognize block fillers during the DOM to view conversion.
+		 *
+		 * @readonly
+		 * @member {Function} engine.treeView.DomConverter#blockFiller
+		 */
+		this.blockFiller = options.blockFiller || BR_FILLER;
 
 		/**
 		 * DOM to View mapping.
@@ -77,28 +97,6 @@ export default class DomConverter {
 		this._viewToDomMapping.set( viewFragment, domFragment );
 	}
 
-	/**
-	 * Compares DOM and View nodes. Elements are same when they are bound. Text nodes are same when they have the same
-	 * text data. Nodes need to have corresponding types. In all other cases nodes are different.
-	 *
-	 * @param {Node} domNode DOM node to compare.
-	 * @param {engine.treeView.Node} viewNode View node to compare.
-	 * @returns {Boolean} True if nodes are same.
-	 */
-	compareNodes( domNode, viewNode ) {
-		// Elements.
-		if ( domNode instanceof HTMLElement && viewNode instanceof ViewElement ) {
-			return domNode === this.getCorrespondingDomElement( viewNode );
-		}
-		// Texts.
-		else if ( domNode instanceof Text && viewNode instanceof ViewText ) {
-			return domNode.data === viewNode.data;
-		}
-
-		// Not matching types.
-		return false;
-	}
-
 	/**
 	 * Converts view to DOM. For all text nodes, not bound elements and document fragments new items will
 	 * be created. For bound elements and document fragments function will return corresponding items.
@@ -110,11 +108,7 @@ export default class DomConverter {
 	 * @param {Boolean} [options.withChildren=true] If true node's and document fragment's children  will be converted too.
 	 * @returns {Node|DocumentFragment} Converted node or DocumentFragment.
 	 */
-	viewToDom( viewNode, domDocument, options ) {
-		if ( !options ) {
-			options = {};
-		}
-
+	viewToDom( viewNode, domDocument, options = {} ) {
 		if ( viewNode instanceof ViewText ) {
 			return domDocument.createTextNode( viewNode.data );
 		} else {
@@ -146,8 +140,8 @@ export default class DomConverter {
 			}
 
 			if ( options.withChildren || options.withChildren === undefined ) {
-				for ( let childView of viewNode.getChildren() ) {
-					domElement.appendChild( this.viewToDom( childView, domDocument, options ) );
+				for ( let child of this.viewChildrenToDom( viewNode, domDocument, options ) ) {
+					domElement.appendChild( child );
 				}
 			}
 
@@ -155,23 +149,124 @@ export default class DomConverter {
 		}
 	}
 
+	/**
+	 * Converts children of the view element to DOM using {@link engine.treeView.DomConverter#viewToDom} method.
+	 * Additionally this method adds block {@link engine.treeView.filler filler} to the list of children, if needed.
+	 *
+	 * @param {engine.treeView.Element|engine.treeView.DocumentFragment} viewElement Parent view element.
+	 * @param {document} domDocument Document which will be used to create DOM nodes.
+	 * @param {Object} options See {@link engine.treeView.DomConverter#viewToDom} options parameter.
+	 * @returns {Iterable.<Node>} DOM nodes.
+	 */
+	*viewChildrenToDom( viewElement, domDocument, options = {} ) {
+		let fillerPositionOffset = viewElement.getFillerOffset && viewElement.getFillerOffset();
+		let offset = 0;
+
+		for ( let childView of viewElement.getChildren() ) {
+			if ( fillerPositionOffset === offset ) {
+				yield this.blockFiller( domDocument );
+			}
+
+			yield this.viewToDom( childView, domDocument, options );
+
+			offset++;
+		}
+
+		if ( fillerPositionOffset === offset ) {
+			yield this.blockFiller( domDocument );
+		}
+	}
+
+	/**
+	 * Converts view {@link engine.treeView.Range} to DOM range.
+	 * Inline and block {@link engine.treeView.filler fillers} are handled during the conversion.
+	 *
+	 * @param {engine.treeView.Range} viewRange View range.
+	 * @returns {Range} DOM range.
+	 */
+	viewRangeToDom( viewRange ) {
+		const domStart = this.viewPositionToDom( viewRange.start );
+		const domEnd = this.viewPositionToDom( viewRange.end );
+
+		const domRange = new Range();
+		domRange.setStart( domStart.parent, domStart.offset );
+		domRange.setEnd( domEnd.parent, domEnd.offset );
+
+		return domRange;
+	}
+
+	/**
+	 * Converts view {@link engine.treeView.Position} to DOM parent and offset.
+	 *
+	 * Inline and block {@link engine.treeView.filler fillers} are handled during the conversion.
+	 * If the converted position is directly before inline filler it is moved inside the filler.
+	 *
+	 * @param {engine.treeView.position} viewPosition View position.
+	 * @returns {Object} position
+	 * @returns {Node} position.parent DOM position parent.
+	 * @returns {Number} position.offset DOM position offset.
+	 */
+	viewPositionToDom( viewPosition ) {
+		const viewParent = viewPosition.parent;
+
+		if ( viewParent instanceof ViewText ) {
+			const domParent = this.getCorrespondingDomText( viewParent );
+			let offset = viewPosition.offset;
+
+			if ( startsWithFiller( domParent ) ) {
+				offset += INLINE_FILLER_LENGTH;
+			}
+
+			return { parent: domParent, offset: offset };
+		}
+		// viewParent instance of ViewElement.
+		else {
+			let domParent, domBefore, domAfter;
+
+			if ( viewPosition.offset === 0 ) {
+				domParent = this.getCorrespondingDom( viewPosition.parent );
+				domAfter = domParent.childNodes[ 0 ];
+			} else {
+				domBefore = this.getCorrespondingDom( viewPosition.nodeBefore );
+				domParent = domBefore.parentNode;
+				domAfter = domBefore.nextSibling;
+			}
+
+			// If there is an inline filler at position return position inside the filler. We should never return
+			// the position before the inline filler.
+			if ( domAfter instanceof Text && startsWithFiller( domAfter ) ) {
+				return { parent: domAfter, offset: INLINE_FILLER_LENGTH };
+			}
+
+			const offset = domBefore ? indexOf( domBefore ) + 1 : 0;
+
+			return { parent: domParent, offset: offset };
+		}
+	}
+
 	/**
 	 * Converts DOM to view. For all text nodes, not bound elements and document fragments new items will
-	 * be created. For bound elements and document fragments function will return corresponding items.
+	 * be created. For bound elements and document fragments function will return corresponding items. For
+	 * {@link engine.treeView.filler fillers} `null` will be returned.
 	 *
 	 * @param {Node|DocumentFragment} domNode DOM node or document fragment to transform.
 	 * @param {Object} [options] Conversion options.
 	 * @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
 	 * @param {Boolean} [options.withChildren=true] It true node's and document fragment's children will be converted too.
-	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment} Converted node or document fragment.
+	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment|null} Converted node or document fragment. Null
+	 * if DOM node is a {@link engine.treeView.filler filler}.
 	 */
-	domToView( domNode, options ) {
-		if ( !options ) {
-			options = {};
+	domToView( domNode, options = {} ) {
+		if ( isBlockFiller( domNode, this.blockFiller )  ) {
+			return null;
 		}
 
 		if ( domNode instanceof Text ) {
-			return new ViewText( domNode.data );
+			if ( isInlineFiller( domNode ) ) {
+				return null;
+			} else {
+				return new ViewText( getDataWithoutFiller( domNode ) );
+			}
 		} else {
 			if ( this.getCorrespondingView( domNode ) ) {
 				return this.getCorrespondingView( domNode );
@@ -179,7 +274,7 @@ export default class DomConverter {
 
 			let viewElement;
 
-			if ( domNode instanceof  DocumentFragment ) {
+			if ( domNode instanceof DocumentFragment ) {
 				// Create view document fragment.
 				viewElement = new ViewDocumentFragment();
 
@@ -203,10 +298,8 @@ export default class DomConverter {
 			}
 
 			if ( options.withChildren || options.withChildren === undefined ) {
-				for ( let i = 0, len = domNode.childNodes.length; i < len; i++ ) {
-					let domChild = domNode.childNodes[ i ];
-
-					viewElement.appendChildren( this.domToView( domChild, options ) );
+				for ( let child of this.domChildrenToView( domNode, options ) ) {
+					viewElement.appendChildren( child );
 				}
 			}
 
@@ -214,6 +307,121 @@ export default class DomConverter {
 		}
 	}
 
+	/**
+	 * Converts children of the DOM element to view nodes using {@link engine.treeView.DomConverter#domToView} method.
+	 * Additionally this method omits block {@link engine.treeView.filler filler}, if it exists in the DOM parent.
+	 *
+	 * @param {HTMLElement} domElement Parent DOM element.
+	 * @param {Object} options See {@link engine.treeView.DomConverter#domToView} options parameter.
+	 * @returns {Iterable.<engine.treeView.Node>} View nodes.
+	 */
+	*domChildrenToView( domElement, options = {} ) {
+		for ( let i = 0; i < domElement.childNodes.length; i++ ) {
+			const domChild = domElement.childNodes[ i ];
+			const viewChild = this.domToView( domChild, options );
+
+			if ( viewChild !== null ) {
+				yield viewChild;
+			}
+		}
+	}
+
+	/**
+	 * Converts DOM selection to view {@link engine.treeView.Selection}.
+	 * Ranges which cannot be converted will be omitted.
+	 *
+	 * @param {Selection} domSelection DOM selection.
+	 * @returns {engine.treeView.Selection} View selection.
+	 */
+	domSelectionToView( domSelection ) {
+		const viewSelection = new ViewSelection();
+
+		for ( let i = 0; i < domSelection.rangeCount; i++ ) {
+			const domRange = domSelection.getRangeAt( i );
+			const viewRange = this.domRangeToView( domRange );
+
+			if ( viewRange ) {
+				viewSelection.addRange( viewRange );
+			}
+		}
+
+		return viewSelection;
+	}
+
+	/**
+	 * Converts DOM Range to view {@link engine.treeView.range}.
+	 * If the start or end position can not be converted `null` is returned.
+	 *
+	 * @param {Range} domRange DOM range.
+	 * @returns {engine.treeView.Range|null} View range.
+	 */
+	domRangeToView( domRange ) {
+		const viewStart = this.domPositionToView( domRange.startContainer, domRange.startOffset );
+		const viewEnd = this.domPositionToView( domRange.endContainer, domRange.endOffset );
+
+		if ( viewStart && viewEnd ) {
+			return new ViewRange( viewStart, viewEnd );
+		}
+
+		return null;
+	}
+
+	/**
+	 * Converts DOM parent and offset to view {@link engine.treeView.Position}.
+	 *
+	 * If the position is inside a {@link engine.treeView.filler filler} which has no corresponding view node,
+	 * position of the filler will be converted and returned.
+	 *
+	 * If structures are too different and it is not possible to find corresponding position then `null` will be returned.
+	 *
+	 * @param {Node} domParent DOM position parent.
+	 * @param {Number} domOffset DOM position offset.
+	 * @returns {engine.treeView.Position} viewPosition View position.
+	 */
+	domPositionToView( domParent, domOffset ) {
+		if ( isBlockFiller( domParent, this.blockFiller ) ) {
+			return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
+		}
+
+		if ( domParent instanceof Text ) {
+			if ( isInlineFiller( domParent ) ) {
+				return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
+			}
+
+			const viewParent = this.getCorrespondingViewText( domParent );
+			let offset = domOffset;
+
+			if ( !viewParent ) {
+				return null;
+			}
+
+			if ( startsWithFiller( domParent ) ) {
+				offset -= INLINE_FILLER_LENGTH;
+				offset = offset < 0 ? 0 : offset;
+			}
+
+			return new ViewPosition( viewParent, offset );
+		}
+		// domParent instanceof HTMLElement.
+		else {
+			if ( domOffset === 0 ) {
+				const viewParent = this.getCorrespondingView( domParent );
+
+				if ( viewParent ) {
+					return new ViewPosition( viewParent, 0 );
+				}
+			} else {
+				const viewBefore = this.getCorrespondingView( domParent.childNodes[ domOffset - 1 ] );
+
+				if ( viewBefore ) {
+					return new ViewPosition( viewBefore.parent, viewBefore.getIndex() + 1 );
+				}
+			}
+
+			return null;
+		}
+	}
+
 	/**
 	 * Gets corresponding view item. This function use
 	 * {@link engine.treeView.DomConverter#getCorrespondingViewElement getCorrespondingViewElement}
@@ -221,6 +429,8 @@ export default class DomConverter {
 	 * nodes and {@link engine.treeView.DomConverter#getCorrespondingViewDocumentFragment getCorrespondingViewDocumentFragment}
 	 * for document fragments.
 	 *
+	 * Note that for the block or inline {@link engine.treeView.filler filler} this method returns `null`.
+	 *
 	 * @param {Node|DocumentFragment} domNode DOM node or document fragment.
 	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment|null} Corresponding view item.
 	 */
@@ -229,17 +439,19 @@ export default class DomConverter {
 			return this.getCorrespondingViewElement( domNode );
 		} else if ( domNode instanceof DocumentFragment ) {
 			return this.getCorrespondingViewDocumentFragment( domNode );
-		} else {
+		} else if ( domNode instanceof Text ) {
 			return this.getCorrespondingViewText( domNode );
 		}
+
+		return null;
 	}
 
 	/**
 	 * Gets corresponding view element. Returns element if an view element was
-	 * {@link engine.treeView.DomConverter#bindElements bound} to the given DOM element or null otherwise.
+	 * {@link engine.treeView.DomConverter#bindElements bound} to the given DOM element or `null` otherwise.
 	 *
 	 * @param {HTMLElement} domElement DOM element.
-	 * @returns {engine.treeView.Element|null} Corresponding element or null if no element was bound.
+	 * @returns {engine.treeView.Element|null} Corresponding element or `null` if no element was bound.
 	 */
 	getCorrespondingViewElement( domElement ) {
 		return this._domToViewMapping.get( domElement );
@@ -247,10 +459,10 @@ export default class DomConverter {
 
 	/**
 	 * Gets corresponding view document fragment. Returns document fragment if an view element was
-	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given DOM fragment or null otherwise.
+	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given DOM fragment or `null` otherwise.
 	 *
 	 * @param {DocumentFragment} domFragment DOM element.
-	 * @returns {engine.treeView.DocumentFragment|null} Corresponding document fragment or null if none element was bound.
+	 * @returns {engine.treeView.DocumentFragment|null} Corresponding document fragment or `null` if none element was bound.
 	 */
 	getCorrespondingViewDocumentFragment( domFragment ) {
 		return this._domToViewMapping.get( domFragment );
@@ -268,11 +480,17 @@ export default class DomConverter {
 	 *
 	 * Otherwise `null` is returned.
 	 *
+	 * Note that for the block or inline {@link engine.treeView.filler filler} this method returns `null`.
+	 *
 	 * @param {Text} domText DOM text node.
-	 * @returns {engine.treeView.Text|null} Corresponding view text node or null, if it was not possible to find a
+	 * @returns {engine.treeView.Text|null} Corresponding view text node or `null`, if it was not possible to find a
 	 * corresponding node.
 	 */
 	getCorrespondingViewText( domText ) {
+		if ( isInlineFiller( domText ) ) {
+			return null;
+		}
+
 		const previousSibling = domText.previousSibling;
 
 		// Try to use previous sibling to find the corresponding text node.
@@ -285,15 +503,29 @@ export default class DomConverter {
 			const viewElement = this.getCorrespondingViewElement( previousSibling );
 
 			if ( viewElement ) {
-				return viewElement.getNextSibling();
+				const nextSibling = viewElement.getNextSibling();
+
+				// It might be filler which has no corresponding view node.
+				if ( nextSibling instanceof ViewText ) {
+					return viewElement.getNextSibling();
+				} else {
+					return null;
+				}
 			}
 		}
 		// Try to use parent to find the corresponding text node.
 		else {
-			const viewElement = this.getCorrespondingViewElement( domText.parentElement );
+			const viewElement = this.getCorrespondingViewElement( domText.parentNode );
 
 			if ( viewElement ) {
-				return viewElement.getChild( 0 );
+				const firstChild = viewElement.getChild( 0 );
+
+				// It might be filler which has no corresponding view node.
+				if ( firstChild instanceof ViewText ) {
+					return firstChild;
+				} else {
+					return null;
+				}
 			}
 		}
 
@@ -322,10 +554,10 @@ export default class DomConverter {
 
 	/**
 	 * Gets corresponding DOM element. Returns element if an DOM element was
-	 * {@link engine.treeView.DomConverter#bindElements bound} to the given view element or null otherwise.
+	 * {@link engine.treeView.DomConverter#bindElements bound} to the given view element or `null` otherwise.
 	 *
 	 * @param {engine.treeView.Element} viewElement View element.
-	 * @returns {HTMLElement|null} Corresponding element or null if none element was bound.
+	 * @returns {HTMLElement|null} Corresponding element or `null` if none element was bound.
 	 */
 	getCorrespondingDomElement( viewElement ) {
 		return this._viewToDomMapping.get( viewElement );
@@ -333,10 +565,10 @@ export default class DomConverter {
 
 	/**
 	 * Gets corresponding DOM document fragment. Returns document fragment if an DOM element was
-	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given view document fragment or null otherwise.
+	 * {@link engine.treeView.DomConverter#bindDocumentFragments bound} to the given view document fragment or `null` otherwise.
 	 *
 	 * @param {engine.treeView.DocumentFragment} viewDocumentFragment View document fragment.
-	 * @returns {DocumentFragment|null} Corresponding document fragment or null if no fragment was bound.
+	 * @returns {DocumentFragment|null} Corresponding document fragment or `null` if no fragment was bound.
 	 */
 	getCorrespondingDomDocumentFragment( viewDocumentFragment ) {
 		return this._viewToDomMapping.get( viewDocumentFragment );
@@ -352,10 +584,10 @@ export default class DomConverter {
 	 * If this is a first child in the parent and the parent is a {@link engine.treeView.DomConverter#bindElements bound}
 	 * element, it is used to find the corresponding text node.
 	 *
-	 * Otherwise null is returned.
+	 * Otherwise `null` is returned.
 	 *
 	 * @param {engine.treeView.Text} viewText View text node.
-	 * @returns {Text|null} Corresponding DOM text node or null, if it was not possible to find a corresponding node.
+	 * @returns {Text|null} Corresponding DOM text node or `null`, if it was not possible to find a corresponding node.
 	 */
 	getCorrespondingDomText( viewText ) {
 		const previousSibling = viewText.getPreviousSibling();

+ 2 - 2
packages/ckeditor5-engine/src/treeview/element.js

@@ -108,7 +108,7 @@ export default class Element extends Node {
 	 *
 	 * @param {Boolean} deep If set to `true` clones element and all its children recursively. When set to `false`,
 	 * element will be cloned without any children.
-	 * @returns {Element} Clone of this element.
+	 * @returns {engine.treeView.Element} Clone of this element.
 	 */
 	clone( deep ) {
 		const childrenClone = [];
@@ -355,7 +355,7 @@ export default class Element extends Node {
 	 * Both elements should have the same name and attributes to be considered as similar. Two similar elements
 	 * can contain different set of children nodes.
 	 *
-	 * @param {Element} otherElement
+	 * @param {engine.treeView.Element} otherElement
 	 * @returns {Boolean}
 	 */
 	isSimilar( otherElement ) {

+ 176 - 0
packages/ckeditor5-engine/src/treeview/filler.js

@@ -0,0 +1,176 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import { keyCodes } from '../../utils/keyboard.js';
+
+/**
+ * Set of utils related to block and inline fillers handling.
+ *
+ * Browsers do not allow to put caret in elements which does not have hight. Because of it, we need to fill all
+ * empty elements which should be selectable with elements or characters called "fillers". Unfortunately there is no one
+ * universal filler, this is why two types are uses:
+ *
+ * * Block filler is an element which fill block elements, like `<p>`. CKEditor uses `<br>` as a block filler during the editing,
+ * as browsers do natively. So instead of an empty `<p>` there will be `<p><br></p>`. The advantage of block filler is that
+ * it is transparent for the selection, so when the caret is before the `<br>` and user presses right arrow he will be
+ * moved to the next paragraph, not after the `<br>`. The disadvantage is that it breaks a block, so it can not be used
+ * in the middle of a line of text. The {@link engine.treeView.filler.BR_FILLER `<br>` filler} can be replaced with any other
+ * character in the data output, for instance {@link engine.treeView.filler.NBSP_FILLER non-breaking space}.
+ *
+ * * Inline filler is a filler which does not break a line of text, so it can be used inside the text, for instance in the empty
+ * `<b>` surrendered by text: `foo<b></b>bar`, if we want to put the caret there. CKEditor uses a sequence of the zero-width
+ * spaces as an {@link engine.treeView.filler.INLINE_FILLER inline filler} having the predetermined
+ * {@link engine.treeView.filler.INLINE_FILLER_LENGTH length}. A sequence is used, instead of a single character to
+ * avoid threating random zero-width spaces as the inline filler. Disadvantage of the inline filler is that it is not
+ * transparent for the selection. The arrow key moves the caret between zero-width spaces characters, so the additional
+ * code is needed to handle the caret.
+ *
+ * Both inline and block fillers are handled by the {@link engine.treeView.renderer renderer} and are not present in the
+ * view.
+ *
+ * @namespace engine.treeView.filler
+ */
+
+/**
+ * `<br> filler creator. This is a function which creates `<br data-cke-filler="true">` element.
+ * It defines how the filler is created.
+ *
+ * @see engine.treeView.filler.NBSP_FILLER_FILLER
+ * @member {Function} engine.treeView.filler.BR_FILLER
+ */
+export const BR_FILLER = ( domDocument ) => {
+	const fillerBr = domDocument.createElement( 'br' );
+	fillerBr.dataset.ckeFiller = true;
+
+	return fillerBr;
+};
+
+/**
+ * Non-breaking space filler creator. This is a function which creates `&nbsp;` text node.
+ * It defines how the filler is created.
+ *
+ * @see engine.treeView.filler.BR_FILLER
+ * @member {Function} engine.treeView.filler.NBSP_FILLER_FILLER
+ */
+export const NBSP_FILLER = ( domDocument ) => domDocument.createTextNode( '&nbsp;' );
+
+/**
+ * Length of the {@link engine.treeView.filler.INLINE_FILLER INLINE_FILLER}.
+ *
+ * @member {Function} engine.treeView.filler.INLINE_FILLER_LENGTH
+ */
+export const INLINE_FILLER_LENGTH = 7;
+
+/**
+ * Inline filler which is sequence of the zero width spaces.
+ *
+ * @member {String} engine.treeView.filler.INLINE_FILLER
+ */
+export let INLINE_FILLER = '';
+
+for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
+	INLINE_FILLER += '\u200b';
+}
+
+/**
+ * Checks if the text node starts with the {@link engine.treeView.filler.INLINE_FILLER inline filler}.
+ *
+ *		startsWithFiller( document.createTextNode( INLINE_FILLER ) ); // true
+ *		startsWithFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ); // true
+ *		startsWithFiller( document.createTextNode( 'foo' ) ); // false
+ *
+ * @param {Text} domText DOM text node.
+ * @returns {Boolean} True if the text node starts with the {@link engine.treeView.filler.INLINE_FILLER inline filler}.
+ */
+export function startsWithFiller( domText ) {
+	return ( domText.data.substr( 0, INLINE_FILLER_LENGTH ) === INLINE_FILLER );
+}
+
+/**
+ * Checks if the text node contains only the {@link engine.treeView.filler.INLINE_FILLER inline filler}.
+ *
+ *		isInlineFiller( document.createTextNode( INLINE_FILLER ) ); // true
+ *		isInlineFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ); // false
+ *
+ * @param {Text} domText DOM text node.
+ * @returns {Boolean} True if the text node contains only the {@link engine.treeView.filler.INLINE_FILLER inline filler}.
+ */
+export function isInlineFiller( domText ) {
+	return domText.data.length == INLINE_FILLER_LENGTH && startsWithFiller( domText );
+}
+
+/**
+ * Get string data from the text node, removing an {@link engine.treeView.filler.INLINE_FILLER inline filler} from it,
+ * if text node contains it.
+ *
+ *		getDataWithoutFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ) == 'foo' // true
+ *		getDataWithoutFiller( document.createTextNode( 'foo' ) ) == 'foo' // true
+ *
+ * @param {Text} domText DOM text node, possible with inline filler.
+ * @returns {String} Data without filler.
+ */
+export function getDataWithoutFiller( domText ) {
+	if ( startsWithFiller( domText ) ) {
+		return domText.data.slice( INLINE_FILLER_LENGTH );
+	} else {
+		return domText.data;
+	}
+}
+
+// Cache block fillers templates to improve performance.
+const templateBlockFillers = new WeakMap();
+
+/**
+ * Checks if the node is an instance of the block filler of the given type.
+ *
+ *		const brFillerInstance = BR_FILLER( document );
+ *		isBlockFiller( brFillerInstance, BR_FILLER ); // true
+ *
+ * @param {Node} domNode DOM node to check.
+ * @param {Function} blockFiller Block filler creator.
+ * @returns {Boolean} True if text node contains only {@link engine.treeView.filler.INLINE_FILLER inline filler}.
+ */
+export function isBlockFiller( domNode, blockFiller ) {
+	let templateBlockFiller = templateBlockFillers.get( blockFiller );
+
+	if ( !templateBlockFiller ) {
+		templateBlockFiller = blockFiller( window.document );
+		templateBlockFillers.set( blockFiller, templateBlockFiller );
+	}
+
+	return domNode.isEqualNode( templateBlockFiller );
+}
+
+/**
+ * Assign key observer which move cursor from the end of the inline filler to the begging of it when
+ * the left arrow is pressed, so the filler does not break navigation.
+ *
+ * @param {engine.treeView.Document} document Document instance we should inject quirks handling on.
+ */
+export function injectQuirksHandling( document ) {
+	document.on( 'keydown', jumpOverInlineFiller );
+}
+
+// Move cursor from the end of the inline filler to the begging of it when, so the filler does not break navigation.
+function jumpOverInlineFiller( evt, data ) {
+	if ( data.keyCode == keyCodes.arrowleft ) {
+		const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();
+
+		if ( domSelection.rangeCount == 1 && domSelection.getRangeAt( 0 ).collapsed ) {
+			const domParent = domSelection.getRangeAt( 0 ).startContainer;
+			const domOffset = domSelection.getRangeAt( 0 ).startOffset;
+
+			if ( startsWithFiller( domParent ) && domOffset <= INLINE_FILLER_LENGTH ) {
+				const domRange = new Range();
+				domRange.setStart( domParent, 0 );
+				domRange.collapse( true );
+				domSelection.removeAllRanges();
+				domSelection.addRange( domRange );
+			}
+		}
+	}
+}

+ 15 - 15
packages/ckeditor5-engine/src/treeview/node.js

@@ -31,12 +31,12 @@ export default class Node {
 		this.parent = null;
 
 		/**
-		 * {@link engine.treeView.TreeView} reference.
+		 * {@link engine.treeView.Document} reference.
 		 *
 		 * @protected
-		 * @member {engine.treeView.TreeView} engine.treeView.Node#_treeView
+		 * @member {engine.treeView.Document} engine.treeView.Node#_document
 		 */
-		this._treeView = null;
+		this._document = null;
 	}
 
 	/**
@@ -89,17 +89,17 @@ export default class Node {
 	}
 
 	/**
-	 * Gets {@link engine.treeView.TreeView} reference. If the node has {@link engine.treeView.TreeView}, assign by
-	 * {@link engine.treeView.Node#setTreeView} it will be returned. Otherwise {@link engine.treeView.TreeView} of the parents node
+	 * Gets {@link engine.treeView.Document} reference. If the node has {@link engine.treeView.Document}, assign by
+	 * {@link engine.treeView.Node#setDocument} it will be returned. Otherwise {@link engine.treeView.Document} of the parents node
 	 * will be returned. If node has no parent, `null` will be returned.
 	 *
-	 * @returns {engine.treeView.TreeView|null} Tree view of the node, tree view of the parent or null.
+	 * @returns {engine.treeView.Document|null} Tree view of the node, tree view of the parent or null.
 	 */
-	getTreeView() {
-		if ( this._treeView ) {
-			return this._treeView;
+	getDocument() {
+		if ( this._document ) {
+			return this._document;
 		} else if ( this.parent ) {
-			return this.parent.getTreeView();
+			return this.parent.getDocument();
 		} else {
 			return null;
 		}
@@ -127,13 +127,13 @@ export default class Node {
 	}
 
 	/**
-	 * Sets the {@link engine.treeView.TreeView} of the node. Note that not all of nodes need to have {@link engine.treeView.TreeView}
-	 * assigned, see {@link engine.treeView.Node#getTreeView}.
+	 * Sets the {@link engine.treeView.Document} of the node. Note that not all of nodes need to have {@link engine.treeView.Document}
+	 * assigned, see {@link engine.treeView.Node#getDocument}.
 	 *
-	 * @param {engine.treeView.TreeView} treeView Tree view.
+	 * @param {engine.treeView.Document} document Document.
 	 */
-	setTreeView( treeView ) {
-		this._treeView = treeView;
+	setDocument( document ) {
+		this._document = document;
 	}
 
 	/**

+ 7 - 7
packages/ckeditor5-engine/src/treeview/observer/domeventdata.js

@@ -8,7 +8,7 @@
 import extend from '../../../utils/lib/lodash/extend.js';
 
 /**
- * Information about a DOM event in context of the {@link engine.treeView.TreeView}.
+ * Information about a DOM event in context of the {@link engine.treeView.Document}.
  * It wraps the native event, which usually should not be used as the wrapper contains
  * additional data (like key code for keyboard events).
  *
@@ -16,18 +16,18 @@ import extend from '../../../utils/lib/lodash/extend.js';
  */
 export default class DomEventData {
 	/**
-	 * @param {engine.treeView.TreeView} treeView The instance of the tree view.
+	 * @param {engine.treeView.Document} document The instance of the tree view Document.
 	 * @param {Event} domEvent The DOM event.
 	 * @param {Object} [additionalData] Additional properties that the instance should contain.
 	 */
-	constructor( treeView, domEvent, additionalData ) {
+	constructor( document, domEvent, additionalData ) {
 		/**
-		 * The instance of the tree view.
+		 * The instance of the document.
 		 *
 		 * @readonly
-		 * @member {engine.treeView.TreeView} engine.treeView.observer.DomEvent#treeView
+		 * @member {engine.treeView.Document} engine.treeView.observer.DomEvent#treeView
 		 */
-		this.treeView = treeView;
+		this.document = document;
 
 		/**
 		 * The DOM event.
@@ -55,7 +55,7 @@ export default class DomEventData {
 	 * @type engine.treeView.Element
 	 */
 	get target() {
-		return this.treeView.domConverter.getCorrespondingViewElement( this.domTarget );
+		return this.document.domConverter.getCorrespondingViewElement( this.domTarget );
 	}
 
 	/**

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

@@ -63,10 +63,10 @@ export default class DomEventObserver extends Observer {
 	}
 
 	/**
-	 * Calls {@link engine.treeView.TreeView#fire} if observer
+	 * Calls {@link engine.treeView.Document#fire} if observer
 	 * {@link engine.treeView.observer.DomEventObserver#isEnabled is enabled}.
 	 *
-	 * @see engine.treeView.TreeView#fire
+	 * @see engine.treeView.Document#fire
 	 * @param {String} eventType The event type (name).
 	 * @param {Event} domEvent The DOM event.
 	 * @param {Object} [additionalData] The additional data which should extend the
@@ -74,7 +74,7 @@ export default class DomEventObserver extends Observer {
 	 */
 	fire( eventType, domEvent, additionalData ) {
 		if ( this.isEnabled ) {
-			this.treeView.fire( eventType, new DomEventData( this.treeView, domEvent, additionalData ) );
+			this.document.fire( eventType, new DomEventData( this.document, domEvent, additionalData ) );
 		}
 	}
 }

+ 5 - 5
packages/ckeditor5-engine/src/treeview/observer/focusobserver.js

@@ -8,14 +8,14 @@
 import DomEventObserver from './domeventobserver.js';
 
 /**
- * {@link engine.treeView.TreeView#focus Focus} and {@link engine.treeView.TreeView#blur blur} events observer.
+ * {@link engine.treeView.Document#focus Focus} and {@link engine.treeView.Document#blur blur} events observer.
  *
  * @memberOf engine.treeView.observer
  * @extends engine.treeView.observer.DomEventObserver
  */
 export default class FocusObserver extends DomEventObserver {
-	constructor( treeView ) {
-		super( treeView );
+	constructor( document ) {
+		super( document );
 
 		this.domEventType = [ 'focus', 'blur' ];
 	}
@@ -28,13 +28,13 @@ export default class FocusObserver extends DomEventObserver {
 /**
  * Fired when one of the editables gets focus.
  *
- * @event engine.treeView.TreeView#focus
+ * @event engine.treeView.Document#focus
  * @param {engine.treeView.observer.DomEventData} data Event data.
  */
 
 /**
  * Fired when one of the editables loses focus.
  *
- * @event engine.treeView.TreeView#blur
+ * @event engine.treeView.Document#blur
  * @param {engine.treeView.observer.DomEventData} data Event data.
  */

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

@@ -9,14 +9,14 @@ import DomEventObserver from './domeventobserver.js';
 import { getCode } from '../../../utils/keyboard.js';
 
 /**
- * {@link engine.treeView.TreeView#keydown Key down} event observer.
+ * {@link engine.treeView.Document#keydown Key down} event observer.
  *
  * @memberOf engine.treeView.observer
  * @extends engine.treeView.observer.DomEventObserver
  */
 export default class KeyObserver extends DomEventObserver {
-	constructor( treeView ) {
-		super( treeView );
+	constructor( document ) {
+		super( document );
 
 		this.domEventType = 'keydown';
 	}
@@ -39,12 +39,12 @@ export default class KeyObserver extends DomEventObserver {
 /**
  * Fired when a key has been pressed.
  *
- * @event engine.treeView.TreeView#keydown
+ * @event engine.treeView.Document#keydown
  * @param {engine.treeView.observer.keyObserver.KeyEventData} keyEventData
  */
 
 /**
- * The value of the {@link engine.treeView.TreeView#keydown} event.
+ * The value of the {@link engine.treeView.Document#keydown} event.
  *
  * @class engine.treeView.observer.keyObserver.KeyEventData
  * @extends engine.treeView.observer.DomEventData

+ 48 - 32
packages/ckeditor5-engine/src/treeview/observer/mutationobserver.js

@@ -6,24 +6,25 @@
 'use strict';
 
 import Observer from './observer.js';
+import { startsWithFiller, getDataWithoutFiller } from '../filler.js';
 
 /**
- * Mutation observer class observes changes in the DOM, fires {@link engine.treeView.TreeView#mutations} event, mark view elements
+ * Mutation observer class observes changes in the DOM, fires {@link engine.treeView.Document#mutations} event, mark view elements
  * as changed and call {@link engine.treeView.render}. Because all mutated nodes are marked as "to be rendered" and the
  * {@link engine.treeView.render} is called, all changes will be reverted, unless the mutation will be handled by the
- * {@link engine.treeView.TreeView#mutations} event listener. It means user will see only handled changes, and the editor will
+ * {@link engine.treeView.Document#mutations} event listener. It means user will see only handled changes, and the editor will
  * block all changes which are not handled.
  *
  * Mutation Observer also take care of reducing number of mutations which are fired. It removes duplicates and
  * mutations on elements which do not have corresponding view elements. Also
- * {@link engine.treeView.TreeView.MutatatedText text mutation} is fired only if parent element do not change child list.
+ * {@link engine.treeView.Document.MutatatedText text mutation} is fired only if parent element do not change child list.
  *
  * @memberOf engine.treeView.observer
  * @extends engine.treeView.observer.Observer
  */
 export default class MutationObserver extends Observer {
-	constructor( treeView ) {
-		super( treeView );
+	constructor( document ) {
+		super( document );
 
 		/**
 		 * Native mutation observer config.
@@ -39,18 +40,18 @@ export default class MutationObserver extends Observer {
 		};
 
 		/**
-		 * Reference to the {@link engine.treeView.TreeView#domConverter}.
+		 * Reference to the {@link engine.treeView.Document#domConverter}.
 		 *
 		 * @member {engine.treeView.DomConverter} engine.treeView.observer.MutationObserver#domConverter
 		 */
-		this.domConverter = treeView.domConverter;
+		this.domConverter = document.domConverter;
 
 		/**
-		 * Reference to the {@link engine.treeView.TreeView#renderer}.
+		 * Reference to the {@link engine.treeView.Document#renderer}.
 		 *
 		 * @member {engine.treeView.Renderer} engine.treeView.observer.MutationObserver#renderer
 		 */
-		this.renderer = treeView.renderer;
+		this.renderer = document.renderer;
 
 		/**
 		 * Observed DOM elements.
@@ -69,6 +70,16 @@ export default class MutationObserver extends Observer {
 		this._mutationObserver = new window.MutationObserver( this._onMutations.bind( this ) );
 	}
 
+	/**
+	 * Synchronously fires {@link engine.treeView.Document#mutations} event with all mutations in record queue.
+	 * At the same time empties the queue so mutations will not be fired twice.
+	 *
+	 * @returns {[type]} [description]
+	 */
+	flush() {
+		this._onMutations( this._mutationObserver.takeRecords() );
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -103,11 +114,18 @@ export default class MutationObserver extends Observer {
 	/**
 	 * Handles mutations. Deduplicates, mark view elements to sync, fire event and call render.
 	 *
-	 * @protected
+	 * @private
 	 * @method engine.treeView.observer.MutationObserver#_onMutations
 	 * @param {Array.<Object>} domMutations Array of native mutations.
 	 */
 	_onMutations( domMutations ) {
+		// As a result of this.flush() we can have an empty collection.
+		if ( domMutations.length === 0 ) {
+			return;
+		}
+
+		const domConverter = this.domConverter;
+
 		// Use map and set for deduplication.
 		const mutatedTexts = new Map();
 		const mutatedElements = new Set();
@@ -116,7 +134,7 @@ export default class MutationObserver extends Observer {
 		// element with changed structure anyway.
 		for ( let mutation of domMutations ) {
 			if ( mutation.type === 'childList' ) {
-				const element = this.domConverter.getCorrespondingViewElement( mutation.target );
+				const element = domConverter.getCorrespondingViewElement( mutation.target );
 
 				if ( element ) {
 					mutatedElements.add( element );
@@ -127,7 +145,7 @@ export default class MutationObserver extends Observer {
 		// Handle `characterData` mutations later, when we have the full list of nodes which changed structure.
 		for ( let mutation of domMutations ) {
 			if ( mutation.type === 'characterData' ) {
-				const text = this.domConverter.getCorrespondingViewText( mutation.target );
+				const text = domConverter.getCorrespondingViewText( mutation.target );
 
 				if ( text && !mutatedElements.has( text.parent ) ) {
 					// Use text as a key, for deduplication. If there will be another mutation on the same text element
@@ -135,10 +153,16 @@ export default class MutationObserver extends Observer {
 					mutatedTexts.set( text, {
 						type: 'text',
 						oldText: text.data,
-						newText: mutation.target.data,
+						newText: getDataWithoutFiller( mutation.target ),
 						node: text
 					} );
 				}
+				// When we added first letter to the text node which had only inline filler, for the DOM it is mutation
+				// on text, but for the view, where filler text node did not existed, new text node was created, so we
+				// need to fire 'children' mutation instead of 'text'.
+				else if ( !text && startsWithFiller( mutation.target ) ) {
+					mutatedElements.add( domConverter.getCorrespondingViewElement( mutation.target.parentNode ) );
+				}
 			}
 		}
 
@@ -150,34 +174,26 @@ export default class MutationObserver extends Observer {
 
 		for ( let mutatedText of mutatedTexts.values() ) {
 			this.renderer.markToSync( 'TEXT', mutatedText.node );
-
 			viewMutations.push( mutatedText );
 		}
 
 		for ( let viewElement of mutatedElements ) {
-			const domElement = this.domConverter.getCorrespondingDomElement( viewElement );
-			const domChildren = domElement.childNodes;
+			const domElement = domConverter.getCorrespondingDomElement( viewElement );
 			const viewChildren = viewElement.getChildren();
-			const newViewChildren = [];
-
-			// We want to have a list of View elements, not DOM elements.
-			for ( let i = 0; i < domChildren.length; i++ ) {
-				newViewChildren.push( this.domConverter.domToView( domChildren[ i ] ) );
-			}
+			const newViewChildren = domConverter.domChildrenToView( domElement );
 
 			this.renderer.markToSync( 'CHILDREN', viewElement );
-
 			viewMutations.push( {
 				type: 'children',
 				oldChildren: Array.from( viewChildren ),
-				newChildren: newViewChildren,
+				newChildren: Array.from( newViewChildren ),
 				node: viewElement
 			} );
 		}
 
-		this.treeView.fire( 'mutations', viewMutations );
+		this.document.fire( 'mutations', viewMutations );
 
-		this.treeView.render();
+		this.document.render();
 	}
 }
 
@@ -185,17 +201,17 @@ 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
  * mutation, so all changes which should be applied, should be handled on this event.
  *
- * @event engine.treeView.TreeView#mutations
- * @param {Array.<engine.treeView.TreeView~MutatatedText|engine.treeView.TreeView~MutatatedChildren>} viewMutations
+ * @event engine.treeView.Document#mutations
+ * @param {Array.<engine.treeView.Document~MutatatedText|engine.treeView.Document~MutatatedChildren>} viewMutations
  * Array of mutations.
- * For mutated texts it will be {@link engine.treeView.TreeView~MutatatedText} and for mutated elements it will be
- * {@link engine.treeView.TreeView~MutatatedElement}. You can recognize the type based on the `type` property.
+ * For mutated texts it will be {@link engine.treeView.Document~MutatatedText} and for mutated elements it will be
+ * {@link engine.treeView.Document~MutatatedElement}. You can recognize the type based on the `type` property.
  */
 
 /**
  * Mutation item for text.
  *
- * @see engine.treeView.TreeView#mutations
+ * @see engine.treeView.Document#mutations
  * @see engine.treeView.MutatatedChildren
  *
  * @typedef {Object} engine.treeView.MutatatedText
@@ -209,7 +225,7 @@ export default class MutationObserver extends Observer {
 /**
  * Mutation item for child nodes.
  *
- * @see engine.treeView.TreeView#mutations
+ * @see engine.treeView.Document#mutations
  * @see engine.treeView.MutatatedText
  *
  * @typedef {Object} engine.treeView.MutatatedChildren

+ 8 - 8
packages/ckeditor5-engine/src/treeview/observer/observer.js

@@ -7,7 +7,7 @@
 
 /**
  * Abstract base observer class. Observers are classes which observe changes on DOM elements, do the preliminary
- * processing and fire events on the {@link engine.treeView.TreeView} objects.
+ * processing and fire events on the {@link engine.treeView.Document} objects.
  *
  * @abstract
  * @memberOf engine.treeView.observer
@@ -16,16 +16,16 @@ export default class Observer {
 	/**
 	 * Creates an instance of the observer.
 	 *
-	 * @param {engine.treeView.TreeView} treeView
+	 * @param {engine.treeView.Document} document
 	 */
-	constructor( treeView ) {
+	constructor( document ) {
 		/**
-		 * Reference to the {@link engine.treeView.TreeView} object.
+		 * Reference to the {@link engine.treeView.Document} object.
 		 *
 		 * @readonly
-		 * @member {engine.treeView.TreeView} engine.treeView.observer.Observer#treeView
+		 * @member {engine.treeView.Document} engine.treeView.observer.Observer#document
 		 */
-		this.treeView = treeView;
+		this.document = document;
 
 		/**
 		 * State of the observer. If it is disabled events will not be fired.
@@ -38,7 +38,7 @@ export default class Observer {
 
 	/**
 	 * Enables the observer. This method is called when then observer is registered to the
-	 * {@link engine.treeView.TreeView} and after {@link engine.treeView.TreeView#render rendering}
+	 * {@link engine.treeView.Document} and after {@link engine.treeView.Document#render rendering}
 	 * (all observers are {@link engine.treeView.observer.Observer#disable disabled} before rendering).
 	 *
 	 * A typical use case for disabling observers is that mutation observers need to be disabled for the rendering.
@@ -52,7 +52,7 @@ export default class Observer {
 
 	/**
 	 * Disables the observer. This method is called before
-	 * {@link engine.treeView.TreeView#render rendering} to prevent firing events during rendering.
+	 * {@link engine.treeView.Document#render rendering} to prevent firing events during rendering.
 	 *
 	 * @see engine.treeView.observer.Observer#enable
 	 */

+ 131 - 0
packages/ckeditor5-engine/src/treeview/observer/selectionobserver.js

@@ -0,0 +1,131 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Observer from './observer.js';
+import MutationObserver from './mutationobserver.js';
+
+/**
+ * Selection observer class observes selection changes in the document. If selection changes on the document this
+ * observer checks if there are any mutations and if DOM selection is different than the
+ * {@link engine.treeView.Document#selection view selection}. Selection observer fires
+ * {@link engine.treeView.Document#selectionChange} event only if selection change was the only change in the document
+ * and DOM selection is different then the view selection.
+ *
+ * @see engine.treeView.MutationObserver
+ * @memberOf engine.treeView.observer
+ * @extends engine.treeView.observer.Observer
+ */
+export default class SelectionObserver extends Observer {
+	constructor( document ) {
+		super( document );
+
+		/**
+		 * Instance of the mutation observer. Selection observer calls
+		 * {@link engine.treeView.observer.MutationObserver#flush} to ensure that the mutations will be handled before the
+		 * {@link engine.treeView.Document#selectionChange} event is fired.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.observer.MutationObserver} engine.treeView.observer.SelectionObserver#mutationObserver
+		 */
+		this.mutationObserver = document.getObserver( MutationObserver );
+
+		/**
+		 * Reference to the {@link engine.treeView.Document} object.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.Document} engine.treeView.observer.SelectionObserver#document
+		 */
+		this.document = document;
+
+		/**
+		 * Reference to the TreeView {@link engine.treeView.Selection} object used to compare new selection with it.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.Document} engine.treeView.observer.SelectionObserver#selection
+		 */
+		this.selection = document.selection;
+
+		/**
+		 * Reference to the {@link engine.treeView.Document#domConverter}.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.DomConverter} engine.treeView.observer.SelectionObserver#domConverter
+		 */
+		this.domConverter = document.domConverter;
+
+		/**
+		 * Set of documents which have added "selectionchange" listener to avoid adding listener twice to the same
+		 * document.
+		 *
+		 * @private
+		 * @member {WeakSet.<Document>} engine.treeView.observer.SelectionObserver#_documents
+		 */
+		this._documents = new WeakSet();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	observe( domElement ) {
+		const domDocument = domElement.ownerDocument;
+
+		// Add listener once per each document.
+		if ( this._documents.has( domDocument ) ) {
+			return;
+		}
+
+		domDocument.addEventListener( 'selectionchange', () => this._handleSelectionChange( domDocument ) );
+
+		this._documents.add( domDocument );
+	}
+
+	/**
+	 * Selection change listener. {@link engine.treeView.observer.MutationObserver#flush Flush} mutations, check if
+	 * selection changes and fires {@link engine.treeView.Document#selectionChange} event.
+	 *
+	 * @private
+	 * @param {Document} domDocument DOM document.
+	 */
+	_handleSelectionChange( domDocument ) {
+		if ( !this.isEnabled ) {
+			return;
+		}
+
+		// Ensure the mutation event will be before selection event on all browsers.
+		this.mutationObserver.flush();
+
+		// If there were mutations then the view will be re-rendered by the mutation observer and selection
+		// will be updated, so selections will equal and event will not be fired, as expected.
+		const domSelection = domDocument.defaultView.getSelection();
+		const newViewSelection = this.domConverter.domSelectionToView( domSelection );
+
+		if ( this.selection.isEqual( newViewSelection ) ) {
+			return;
+		}
+
+		// Should be fired only when selection change was the only document change.
+		this.document.fire( 'selectionChange', {
+			oldSelection: this.selection,
+			newSelection: newViewSelection,
+			domSelection: domSelection
+		} );
+
+		this.document.render();
+	}
+}
+
+/**
+ * Fired when selection has changed. This event is fired only when the selection change was the only change that happened
+ * in the document, and old selection is different then the new selection.
+ *
+ * @event engine.treeView.Document#selectionChange
+ * @param {Object} data
+ * @param {engine.treeView.Selection} data.oldSelection Old View selection which is
+ * {@link engine.treeView.Document#selection}.
+ * @param {engine.treeView.Selection} data.newSelection New View selection which is converted DOM selection.
+ * @param {Selection} data.domSelection Native DOM selection.
+ */

+ 46 - 4
packages/ckeditor5-engine/src/treeview/position.js

@@ -5,9 +5,11 @@
 
 'use strict';
 
-import compareArrays from '../../utils/comparearrays.js';
 import Text from './text.js';
 
+import compareArrays from '../../utils/comparearrays.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
+
 /**
  * Position in the tree. Position is always located before or after a node.
  *
@@ -41,7 +43,7 @@ export default class Position {
 	 * inside text node.
 	 *
 	 * @readonly
-	 * @type {engine.treeView.Node}
+	 * @type {engine.treeView.Node|null}
 	 */
 	get nodeAfter() {
 		if ( this.parent instanceof Text ) {
@@ -56,7 +58,7 @@ export default class Position {
 	 * inside text node.
 	 *
 	 * @readonly
-	 * @type {engine.treeView.Node}
+	 * @type {engine.treeView.Node|null}
 	 */
 	get nodeBefore() {
 		if ( this.parent instanceof Text ) {
@@ -185,7 +187,47 @@ export default class Position {
 	}
 
 	/**
-	 * Creates and returns a new instance of Position, which is equal to passed position.
+	 * Creates a new position after the given node.
+	 *
+	 * @param {engine.treeView.Node} node Node after which the position should be located.
+	 * @returns {engine.treeView.Position}
+	 */
+	static createAfter( node ) {
+		if ( !node.parent ) {
+			/**
+			 * You can not make a position after a root.
+			 *
+			 * @error position-after-root
+			 * @param {engine.treeView.Node} root
+			 */
+			throw new CKEditorError( 'position-after-root: You can not make position after root.', { root: node } );
+		}
+
+		return new Position( node.parent, node.getIndex() + 1 );
+	}
+
+	/**
+	 * Creates a new position before the given node.
+	 *
+	 * @param {engine.treeView.node} node Node before which the position should be located.
+	 * @returns {engine.treeView.Position}
+	 */
+	static createBefore( node ) {
+		if ( !node.parent ) {
+			/**
+			 * You cannot make a position before a root.
+			 *
+			 * @error position-before-root
+			 * @param {engine.treeView.Node} root
+			 */
+			throw new CKEditorError( 'position-before-root: You can not make position before root.', { root: node } );
+		}
+
+		return new Position( node.parent, node.getIndex() );
+	}
+
+	/**
+	 * Creates and returns a new instance of `Position`, which is equal to the passed position.
 	 *
 	 * @param {engine.treeView.Position} position Position to be cloned.
 	 * @returns {engine.treeView.Position}

+ 309 - 56
packages/ckeditor5-engine/src/treeview/renderer.js

@@ -5,15 +5,28 @@
 
 'use strict';
 
+import ViewText from './text.js';
+import ViewElement from './element.js';
+import ViewPosition from './position.js';
+import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller, isBlockFiller } from './filler.js';
+
 import diff from '../../utils/diff.js';
+import insertAt from '../../utils/dom/insertat.js';
+import remove from '../../utils/dom/remove.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
- * Renderer updates DOM tree, to make it a reflection of the view tree. Changed nodes need to be
- * {@link engine.treeView.Renderer#markToSync marked} to be rendered. Then, on {@link engine.treeView.Renderer#render render}, renderer
- * ensure they need to be refreshed and creates DOM nodes from view nodes,
- * {@link engine.treeView.DomConverter#bindElements bind} them and insert into DOM tree. Renderer use {@link engine.treeView.DomConverter}
- * to transform and bind nodes.
+ * Renderer updates DOM structure and selection, to make them a reflection of the view structure and selection.
+ *
+ * View nodes which may need to be rendered needs to be {@link engine.treeView.Renderer#markToSync marked}.
+ * Then, on {@link engine.treeView.Renderer#render render}, renderer compares the view nodes with the DOM nodes
+ * in order to check which ones really need to be refreshed. Finally, it creates DOM nodes from these view nodes,
+ * {@link engine.treeView.DomConverter#bindElements binds} them and inserts into the DOM tree.
+ *
+ * Every time {@link engine.treeView.Renderer#render render} is called, renderer additionally checks if
+ * {@link engine.treeView.Renderer#selection selection} needs update and updates it if so.
+ *
+ * Renderer uses {@link engine.treeView.DomConverter} to transform and bind nodes.
  *
  * @memberOf engine.treeView
  */
@@ -22,8 +35,9 @@ export default class Renderer {
 	 * Creates a renderer instance.
 	 *
 	 * @param {engine.treeView.DomConverter} domConverter Converter instance.
+	 * @param {engine.treeView.Selection} selection View selection.
 	 */
-	constructor( domConverter ) {
+	constructor( domConverter, selection ) {
 		/**
 		 * Converter instance.
 		 *
@@ -55,6 +69,35 @@ export default class Renderer {
 		 * @member {Set.<engine.treeView.Node>} engine.treeView.Renderer#markedTexts
 		 */
 		this.markedTexts = new Set();
+
+		/**
+		 * View selection. Renderer updates DOM Selection to make it match this one.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.Selection} engine.treeView.Renderer#selection
+		 */
+		this.selection = selection;
+
+		/**
+		 * Position of the inline {@link engine.treeView.filler filler}.
+		 * It should always be put BEFORE the text which contains filler.
+		 *
+		 * @private
+		 * @member {engine.treeView.Position} engine.treeView.Renderer#_inlineFillerPosition
+		 */
+		this._inlineFillerPosition = null;
+
+		/**
+		 * Last DOM selection object.
+		 *
+		 * Because renderer handles multiple roots, and because these roots might be in different documents (in case of
+		 * using `iframes`) renderer needs to keep last DOM selection object to remove ranges from it before new selection
+		 * is rendered.
+		 *
+		 * @private
+		 * @member {Selection} engine.treeView.Renderer#_domSelection
+		 */
+		this._domSelection = null;
 	}
 
 	/**
@@ -97,92 +140,302 @@ export default class Renderer {
 	}
 
 	/**
-	 * Render method check {@link engine.treeView.Renderer#markedAttributes}, {@link engine.treeView.Renderer#markedChildren} and
-	 * {@link engine.treeView.Renderer#markedTexts} and updated all nodes which needs to be updated. Then it clear all three
-	 * sets.
+	 * Render method checks {@link engine.treeView.Renderer#markedAttributes},
+	 * {@link engine.treeView.Renderer#markedChildren} and {@link engine.treeView.Renderer#markedTexts} and updats all
+	 * nodes which need to be updated. Then it clears all three sets. Also, every time render is called it compares and
+	 * if needed updates the selection.
 	 *
-	 * Renderer try not to break IME, so it do as little as it is possible to update DOM.
+	 * Renderer tries not to break text composition (e.g. IME) and x-index of the selection,
+	 * so it does as little as it is needed to update the DOM.
 	 *
-	 * For attributes it adds new attributes to DOM elements, update attributes with different values and remove
-	 * attributes which does not exists in the view element.
+	 * For attributes it adds new attributes to DOM elements, updates values and removes
+	 * attributes which do not exist in the view element.
 	 *
-	 * For text nodes it update the text string if it is different. Note that if parent element is marked as an element
+	 * For text nodes it updates the text string if it is different. Note that if parent element is marked as an element
 	 * which changed child list, text node update will not be done, because it may not be possible do find a
-	 * {@link engine.treeView.DomConverter#getCorrespondingDomText corresponding DOM text}. The change will be handled in the
-	 * parent element.
+	 * {@link engine.treeView.DomConverter#getCorrespondingDomText corresponding DOM text}. The change will be handled
+	 * in the parent element.
+	 *
+	 * For elements, which child lists have changed, it calculates a {@link diff} and adds or removs children which have changed.
 	 *
-	 * For nodes which changed child list it calculates a {@link diff} using {@link engine.treeView.DomConverter#compareNodes}
-	 * and add or removed nodes which changed.
+	 * Rendering also handles {@link engine.treeView.filler fillers}. Especially, it checks if the inline filler is needed
+	 * at selection position and adds or removes it. To prevent breaking text composition inline filler will not be
+	 * removed as long selection is in the text node which needed it at first.
 	 */
 	render() {
-		const domConverter = this.domConverter;
+		if ( !this._isInlineFillerAtSelection() ) {
+			this._removeInlineFiller();
+
+			if ( this._needAddInlineFiller() ) {
+				this._inlineFillerPosition = this.selection.getFirstPosition();
+				this.markedChildren.add( this._inlineFillerPosition.parent );
+			} else {
+				this._inlineFillerPosition = null;
+			}
+		}
 
 		for ( let node of this.markedTexts ) {
-			if ( !this.markedChildren.has( node.parent ) && domConverter.getCorrespondingDom( node.parent ) ) {
-				updateText( node );
+			if ( !this.markedChildren.has( node.parent ) && this.domConverter.getCorrespondingDom( node.parent ) ) {
+				this._updateText( node );
 			}
 		}
 
 		for ( let element of this.markedAttributes ) {
-			updateAttrs( element );
+			this._updateAttrs( element );
 		}
 
 		for ( let element of this.markedChildren ) {
-			updateChildren( element );
+			this._updateChildren( element );
 		}
 
+		this._updateSelection();
+
 		this.markedTexts.clear();
 		this.markedAttributes.clear();
 		this.markedChildren.clear();
+	}
 
-		function updateText( viewText ) {
-			const domText = domConverter.getCorrespondingDom( viewText );
+	/**
+	 * Returns `true` if the inline filler and selection are in the same place.
+	 * If it is true it means filler had been added for a reason and selection does not
+	 * left text node, user can be in the middle of the composition so it should not be touched.
+	 *
+	 * @private
+	 * @returns {Boolean} True if the inline filler and selection are in the same place.
+	 */
+	_isInlineFillerAtSelection() {
+		if ( this.selection.rangeCount != 1 || !this.selection.isCollapsed ) {
+			return false;
+		}
+
+		const selectionPosition = this.selection.getFirstPosition();
+		const fillerPosition = this._inlineFillerPosition;
+
+		if ( !fillerPosition ) {
+			return false;
+		}
 
-			if ( domText.data != viewText.data ) {
-				domText.data = viewText.data;
+		if ( fillerPosition.isEqual( selectionPosition )  ) {
+			return true;
+		}
+
+		if ( selectionPosition.parent instanceof ViewText ) {
+			if ( fillerPosition.isEqual( ViewPosition.createBefore( selectionPosition.parent ) ) ) {
+				return true;
 			}
 		}
 
-		function updateAttrs( viewElement ) {
-			const domElement = domConverter.getCorrespondingDom( viewElement );
-			const domAttrKeys = Array.from( domElement.attributes ).map( attr => attr.name );
-			const viewAttrKeys = viewElement.getAttributeKeys();
+		return false;
+	}
+
+	/**
+	 * Removes inline filler.
+	 *
+	 * @private
+	 */
+	_removeInlineFiller() {
+		if ( !this._inlineFillerPosition ) {
+			// Nothing to remove.
+			return;
+		}
+
+		const domFillerPosition = this.domConverter.viewPositionToDom( this._inlineFillerPosition );
+		const domFillerNode = domFillerPosition.parent;
+
+		// If there is no filler viewPositionToDom will return parent node, so domFillerNode will be an element.
+		if ( !( domFillerNode instanceof Text ) || !startsWithFiller( domFillerNode ) ) {
+			/**
+			 * No inline filler on expected position.
+			 *
+			 * @error renderer-render-no-inline-filler.
+			 */
+			throw new CKEditorError( 'renderer-render-no-inline-filler: No inline filler on expected position.' );
+		}
+
+		if ( isInlineFiller( domFillerNode ) ) {
+			domFillerNode.parentNode.removeChild( domFillerNode );
+		} else {
+			domFillerNode.data = domFillerNode.data.substr( INLINE_FILLER_LENGTH );
+		}
+	}
+
+	/**
+	 * Checks if the inline {@link engine.treeView.filler fillers} should be added.
+	 *
+	 * @private
+	 * @returns {Boolean} True if the inline fillers should be added.
+	 */
+	_needAddInlineFiller() {
+		if ( this.selection.rangeCount != 1 || !this.selection.isCollapsed ) {
+			return false;
+		}
+
+		const selectionPosition = this.selection.getFirstPosition();
+		const selectionParent = selectionPosition.parent;
+		const selectionOffset = selectionPosition.offset;
 
-			// Add or overwrite attributes.
-			for ( let key of viewAttrKeys ) {
-				domElement.setAttribute( key, viewElement.getAttribute( key ) );
+		if ( !( selectionParent instanceof ViewElement ) ) {
+			return false;
+		}
+
+		// We have block filler, we do not need inline one.
+		if ( selectionOffset === selectionParent.getFillerOffset() ) {
+			return false;
+		}
+
+		const nodeBefore = selectionPosition.nodeBefore;
+		const nodeAfter = selectionPosition.nodeAfter;
+
+		if ( nodeBefore instanceof ViewText || nodeAfter instanceof ViewText ) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Checks if text needs to be updated and possibly updates it.
+	 *
+	 * @private
+	 * @param {engine.treeView.Text} viewText View text to update.
+	 */
+	_updateText( viewText ) {
+		const domText = this.domConverter.getCorrespondingDom( viewText );
+
+		const actualText = domText.data;
+		let expectedText = viewText.data;
+
+		const filler = this._inlineFillerPosition;
+
+		if ( filler && filler.parent == viewText.parent && filler.offset == viewText.getIndex() ) {
+			expectedText = INLINE_FILLER + expectedText;
+		}
+
+		if ( actualText != expectedText ) {
+			domText.data = expectedText;
+		}
+	}
+
+	/**
+	 * Checks if attributes list needs to be updated and possibly updates it.
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} viewElement View element to update.
+	 */
+	_updateAttrs( viewElement ) {
+		const domElement = this.domConverter.getCorrespondingDom( viewElement );
+		const domAttrKeys = Array.from( domElement.attributes ).map( attr => attr.name );
+		const viewAttrKeys = viewElement.getAttributeKeys();
+
+		// Add or overwrite attributes.
+		for ( let key of viewAttrKeys ) {
+			domElement.setAttribute( key, viewElement.getAttribute( key ) );
+		}
+
+		// Remove from DOM attributes which do not exists in the view.
+		for ( let key of domAttrKeys ) {
+			if ( !viewElement.hasAttribute( key ) ) {
+				domElement.removeAttribute( key );
 			}
+		}
+	}
+
+	/**
+	 * Checks if elements child list needs to be updated and possibly updates it.
+	 *
+	 * @private
+	 * @param {engine.treeView.Element} viewElement View element to update.
+	 */
+	_updateChildren( viewElement ) {
+		const domConverter = this.domConverter;
+		const domElement = domConverter.getCorrespondingDom( viewElement );
+		const domDocument = domElement.ownerDocument;
+
+		const filler = this._inlineFillerPosition;
+
+		const actualDomChildren = domElement.childNodes;
+		const expectedDomChildren = Array.from( domConverter.viewChildrenToDom( viewElement, domDocument, { bind: true } ) );
 
-			// Remove from DOM attributes which do not exists in the view.
-			for ( let key of domAttrKeys ) {
-				if ( !viewElement.hasAttribute( key ) ) {
-					domElement.removeAttribute( key );
-				}
+		if ( filler && filler.parent == viewElement ) {
+			const expectedNoteAfterFiller = expectedDomChildren[ filler.offset ];
+
+			if ( expectedNoteAfterFiller instanceof Text ) {
+				expectedNoteAfterFiller.data = INLINE_FILLER + expectedNoteAfterFiller.data;
+			} else {
+				expectedDomChildren.splice( filler.offset, 0, domDocument.createTextNode( INLINE_FILLER ) );
 			}
 		}
 
-		function updateChildren( viewElement ) {
-			const domElement = domConverter.getCorrespondingDom( viewElement );
-			const domChildren = domElement.childNodes;
-			const viewChildren = Array.from( viewElement.getChildren() );
-			const domDocument = domElement.ownerDocument;
+		const actions = diff( actualDomChildren, expectedDomChildren, sameNodes );
 
-			const actions = diff( domChildren, viewChildren,
-				( domNode, viewNode ) => domConverter.compareNodes( domNode, viewNode ) );
+		let i = 0;
 
-			let i = 0;
+		for ( let action of actions ) {
+			if ( action === 'INSERT' ) {
+				insertAt( domElement, i, expectedDomChildren[ i ] );
+				i++;
+			} else if ( action === 'DELETE' ) {
+				remove( actualDomChildren[ i ] );
+			} else { // 'EQUAL'
+				i++;
+			}
+		}
 
-			for ( let action of actions ) {
-				if ( action === 'INSERT' ) {
-					let domChildToInsert = domConverter.viewToDom( viewChildren[ i ], domDocument, { bind: true } );
-					domElement.insertBefore( domChildToInsert, domChildren[ i ] || null );
-					i++;
-				} else if ( action === 'DELETE' ) {
-					domElement.removeChild( domChildren[ i ] );
-				} else { // 'EQUAL'
-					i++;
-				}
+		function sameNodes( actualDomChild, expectedDomChild ) {
+			// Elements.
+			if ( actualDomChild === expectedDomChild ) {
+				return true;
+			}
+			// Texts.
+			else if ( actualDomChild instanceof Text && expectedDomChild instanceof Text ) {
+				return actualDomChild.data === expectedDomChild.data;
 			}
+			// Block fillers.
+			else if ( isBlockFiller( actualDomChild, domConverter.blockFiller ) &&
+				isBlockFiller( expectedDomChild, domConverter.blockFiller ) ) {
+				return true;
+			}
+
+			// Not matching types.
+			return false;
+		}
+	}
+
+	/**
+	 * Checks if selection needs to be updated and possibly updates it.
+	 *
+	 * @private
+	 */
+	_updateSelection() {
+		let domSelection = this._domSelection;
+		const oldViewSelection = domSelection && this.domConverter.domSelectionToView( domSelection );
+
+		if ( !oldViewSelection && !this.selection.rangeCount ) {
+			return;
+		}
+
+		if ( oldViewSelection && this.selection.isEqual( oldViewSelection ) ) {
+			return;
+		}
+
+		if ( domSelection ) {
+			domSelection.removeAllRanges();
 		}
+
+		domSelection = null;
+
+		for ( let range of this.selection.getRanges() ) {
+			const domRangeStart = this.domConverter.viewPositionToDom( range.start );
+			const domRangeEnd = this.domConverter.viewPositionToDom( range.end );
+
+			domSelection = domSelection || domRangeStart.parent.ownerDocument.defaultView.getSelection();
+
+			const domRange = new Range();
+			domRange.setStart( domRangeStart.parent, domRangeStart.offset );
+			domRange.setEnd( domRangeEnd.parent, domRangeEnd.offset );
+			domSelection.addRange( domRange );
+		}
+
+		this._domSelection = domSelection;
 	}
 }

+ 51 - 10
packages/ckeditor5-engine/src/treeview/selection.js

@@ -35,7 +35,7 @@ export default class Selection {
 		/**
 		 * Stores all ranges that are selected.
 		 *
-		 * @private
+		 * @protected
 		 * @member {Array.<engine.treeView.Range>} engine.treeView.Selection#_ranges
 		 */
 		this._ranges = [];
@@ -43,7 +43,7 @@ export default class Selection {
 		/**
 		 * Specifies whether the last added range was added as a backward or forward range.
 		 *
-		 * @private
+		 * @protected
 		 * @member {Boolean} engine.treeView.Selection#_lastRangeBackward
 		 */
 		this._lastRangeBackward = false;
@@ -85,18 +85,13 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns whether the selection is collapsed. Selection is collapsed when all it's ranges are collapsed.
+	 * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
+	 * collapsed.
 	 *
 	 * @type {Boolean}
 	 */
 	get isCollapsed() {
-		for ( let range of this._ranges ) {
-			if ( !range.isCollapsed ) {
-				return false;
-			}
-		}
-
-		return true;
+		return this.rangeCount === 1 && this._ranges[ 0 ].isCollapsed;
 	}
 
 	/**
@@ -108,6 +103,15 @@ export default class Selection {
 		return this._ranges.length;
 	}
 
+	/**
+	 * Specifies whether the {@link engine.treeView.Selection#focus} precedes {@link engine.treeView.Selection#anchor}.
+	 *
+	 * @type {Boolean}
+	 */
+	get isBackward() {
+		return !this.isCollapsed && this._lastRangeBackward;
+	}
+
 	/**
 	 * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
 	 * selection instance and you can safely operate on it.
@@ -206,6 +210,28 @@ export default class Selection {
 		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
 	}
 
+	/**
+	 * Two selections equal if they have the same ranges and directions.
+	 *
+	 * @param {engine.treeView.Selection} otherSelection Selection to compare with.
+	 * @returns {Boolean} True if selections equal.
+	 */
+	isEqual( otherSelection ) {
+		const rangeCount = this.rangeCount;
+
+		if ( rangeCount != otherSelection.rangeCount ) {
+			return false;
+		}
+
+		for ( let i = 0; i < this.rangeCount; i++ ) {
+			if ( !this._ranges[ i ].isEqual( otherSelection._ranges[ i ] ) ) {
+				return false;
+			}
+		}
+
+		return this._lastRangeBackward === otherSelection._lastRangeBackward;
+	}
+
 	/**
 	 * Removes all ranges that were added to the selection.
 	 *
@@ -240,6 +266,21 @@ export default class Selection {
 		this.fire( 'change' );
 	}
 
+	/**
+	 * Set this selection's ranges and direction to the ranges and direction of the given selection.
+	 *
+	 * @param {engine.treeView.Selection} otherSelection Other selection.
+	 */
+	setTo( otherSelection ) {
+		this.removeAllRanges();
+
+		for ( let range of otherSelection.getRanges() ) {
+			this._pushRange( range );
+		}
+
+		this._lastRangeBackward = otherSelection._lastRangeBackward;
+	}
+
 	/**
 	 * Collapses selection to the {@link engine.treeView.Selection#getFirstPosition first position} in stored ranges.
 	 * All ranges will be removed beside one collapsed range. Nothing will be changed if there are no ranges stored

+ 0 - 0
packages/ckeditor5-engine/src/treeview.jsdoc → packages/ckeditor5-engine/src/treeview/treeview.jsdoc


+ 73 - 38
packages/ckeditor5-engine/src/treeview/writer.js

@@ -46,20 +46,39 @@ import isIterable from '../../utils/isiterable.js';
 	 *
 	 * In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes:
 	 *
-	 *		<p>{foo}<b><u>{bar}|</u></b></p> -> <p>{foo}<b><u>{bar}</u></b>|</p>
-	 *		<p>{foo}<b><u>|{bar}</u></b></p> -> <p>{foo}|<b><u>{bar}</u></b></p>
-	 *		<p>{foo}<b><u>{b|ar}</u></b></p> -> <p>{foo}<b><u>{b}</u></b>|<b><u>{ar}</u></b></p>
-	 *
-	 * @see engine.treeView.Writer#isContainer
-	 * @see engine.treeView.Writer#isAttribute
+	 *		<p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
+	 *		<p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
+	 *		<p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
 	 *
+	 * @see engine.treeView.AttributeElement
+	 * @see engine.treeView.ContainerElement
 	 * @param {engine.treeView.Position} position Position where to break attributes.
 	 * @returns {engine.treeView.Position} New position after breaking the attributes.
 	 */
 	breakAttributes( position ) {
+		return this._breakAttributes( position, false );
+	}
+
+	/**
+	 * Private method used by both public breakAttributes (without splitting text nodes) and by other methods (with
+	 * splitting text nodes).
+	 *
+	 * @private
+	 * @param {engine.treeView.Position} position Position where to break attributes.
+	 * @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
+	 * container element. This behavior will result in incorrect view state, but is needed by other `Writer` methods
+	 * which then fixes view state. Defaults to `false`.
+	 * @returns {engine.treeView.Position} New position after breaking the attributes.
+	 */
+	_breakAttributes( position, forceSplitText = false ) {
 		const positionOffset = position.offset;
 		const positionParent = position.parent;
 
+		// There are no attributes to break and text nodes breaking is not forced.
+		if ( !forceSplitText && positionParent instanceof Text && positionParent.parent instanceof ContainerElement ) {
+			return Position.createFromPosition( position );
+		}
+
 		// Position's parent is container, so no attributes to break.
 		if ( positionParent instanceof ContainerElement ) {
 			return Position.createFromPosition( position );
@@ -67,31 +86,31 @@ import isIterable from '../../utils/isiterable.js';
 
 		// Break text and start again in new position.
 		if ( positionParent instanceof Text ) {
-			return this.breakAttributes( breakTextNode( position ) );
+			return this._breakAttributes( breakTextNode( position ), forceSplitText );
 		}
 
 		const length = positionParent.getChildCount();
 
-		// <p>foo<b><u>bar|</u></b></p>
-		// <p>foo<b><u>bar</u>|</b></p>
-		// <p>foo<b><u>bar</u></b>|</p>
+		// <p>foo<b><u>bar{}</u></b></p>
+		// <p>foo<b><u>bar</u>[]</b></p>
+		// <p>foo<b><u>bar</u></b>[]</p>
 		if ( positionOffset == length ) {
 			const newPosition = new Position( positionParent.parent, positionParent.getIndex() + 1 );
 
-			return this.breakAttributes( newPosition );
+			return this._breakAttributes( newPosition, forceSplitText );
 		} else
-		// <p>foo<b><u>|bar</u></b></p>
-		// <p>foo<b>|<u>bar</u></b></p>
-		// <p>foo|<b><u>bar</u></b></p>
+		// <p>foo<b><u>{}bar</u></b></p>
+		// <p>foo<b>[]<u>bar</u></b></p>
+		// <p>foo{}<b><u>bar</u></b></p>
 		if ( positionOffset === 0 ) {
 			const newPosition = new Position( positionParent.parent, positionParent.getIndex() );
 
-			return this.breakAttributes( newPosition );
+			return this._breakAttributes( newPosition, forceSplitText );
 		}
-		// <p>foo<b><u>"b|ar"</u></b></p>
-		// <p>foo<b><u>"b"|"ar"</u></b></p>
-		// <p>foo<b><u>b</u>|<u>ar</u></b></p>
-		// <p>foo<b><u>b</u></b>|<b><u>ar</u></b></p>
+		// <p>foo<b><u>b{}ar</u></b></p>
+		// <p>foo<b><u>b[]ar</u></b></p>
+		// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
+		// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
 		else {
 			const offsetAfter = positionParent.getIndex() + 1;
 
@@ -111,12 +130,12 @@ import isIterable from '../../utils/isiterable.js';
 			// Create new position to work on.
 			const newPosition = new Position( positionParent.parent, offsetAfter );
 
-			return this.breakAttributes( newPosition );
+			return this._breakAttributes( newPosition, forceSplitText );
 		}
 	}
 
 	/**
-	 * Uses {@link engine.treeView.Writer#breakAttributes breakAttribute} method to break attributes on
+	 * Uses {@link engine.treeView.Writer#breakAttributes breakAttributes} method to break attributes on
 	 * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions of
 	 * provided {@link engine.treeView.Range Range}.
 	 *
@@ -129,6 +148,22 @@ import isIterable from '../../utils/isiterable.js';
 	 * @returns {engine.treeView.Range} New range with located at break positions.
 	 */
 	breakRange( range ) {
+		return this._breakRange( range );
+	}
+
+	/**
+	 * Private method used by both public breakRange (without splitting text nodes) and by other methods (with
+	 * splitting text nodes).
+	 *
+	 * @private
+	 * @see engine.treeView.Writer#_breakAttribute
+	 * @param {engine.treeView.Range} range Range which `start` and `end` positions will be used to break attributes.
+	 * @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
+	 * container element. This behavior will result in incorrect view state, but is needed by other `Writer` methods
+	 * which then fixes view state. Defaults to `false`.
+	 * @returns {engine.treeView.Range} New range with located at break positions.
+	 */
+	_breakRange( range, forceSplitText = false ) {
 		const rangeStart = range.start;
 		const rangeEnd = range.end;
 
@@ -144,14 +179,14 @@ import isIterable from '../../utils/isiterable.js';
 
 		// Break at the collapsed position. Return new collapsed range.
 		if ( range.isCollapsed ) {
-			const position = this.breakAttributes( range.start );
+			const position = this._breakAttributes( range.start, forceSplitText );
 
 			return new Range( position, position );
 		}
 
-		const breakEnd = this.breakAttributes( rangeEnd );
+		const breakEnd = this._breakAttributes( rangeEnd, forceSplitText );
 		const count = breakEnd.parent.getChildCount();
-		const breakStart = this.breakAttributes( rangeStart );
+		const breakStart = this._breakAttributes( rangeStart, forceSplitText );
 
 		// Calculate new break end offset.
 		breakEnd.offset += breakEnd.parent.getChildCount() - count;
@@ -165,12 +200,12 @@ import isIterable from '../../utils/isiterable.js';
 	 *
 	 * In following examples `<p>` is a container and `<b>` is an attribute node:
 	 *
-	 *		<p>{foo}|{bar}</p> -> <p>{foo|bar}</p>
-	 *		<p><b>{foo}</b>|<b>{bar}</b> -> <p><b>{foo|bar}</b></b>
-	 *		<p><b foo="bar">{a}</b>|<b foo="baz">{b}</b> -> <p><b foo="bar">{a}</b>|<b foo="baz">{b}</b>
+	 *		<p>foo[]bar</p> -> <p>foo{}bar</p>
+	 *		<p><b>foo</b>[]<b>bar</b> -> <p><b>foo{}bar</b></b>
+	 *		<p><b foo="bar">a</b>[]<b foo="baz">b</b> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b>
 	 *
-	 * @see engine.treeView.Writer#isContainer
-	 * @see engine.treeView.Writer#isAttribute
+	 * @see engine.treeView.AttributeElement
+	 * @see engine.treeView.ContainerElement
 	 * @param {engine.treeView.Position} position Merge position.
 	 * @returns {engine.treeView.Position} Position after merge.
 	 */
@@ -238,7 +273,7 @@ import isIterable from '../../utils/isiterable.js';
 		validateNodesToInsert( nodes );
 
 		const container = this.getParentContainer( position );
-		const insertionPosition = this.breakAttributes( position );
+		const insertionPosition = this._breakAttributes( position, true );
 
 		const length = container.insertChildren( insertionPosition.offset, nodes );
 		const endPosition = insertionPosition.getShiftedBy( length );
@@ -287,7 +322,7 @@ import isIterable from '../../utils/isiterable.js';
 		}
 
 		// Break attributes at range start and end.
-		const { start: breakStart, end: breakEnd } = this.breakRange( range );
+		const { start: breakStart, end: breakEnd } = this._breakRange( range, true );
 		const parentContainer = breakStart.parent;
 
 		const count = breakEnd.offset - breakStart.offset;
@@ -376,7 +411,7 @@ import isIterable from '../../utils/isiterable.js';
 		}
 
 		// Break attributes at range start and end.
-		const { start: breakStart, end: breakEnd } = this.breakRange( range );
+		const { start: breakStart, end: breakEnd } = this._breakRange( range, true );
 		const parentContainer = breakStart.parent;
 
 		// Unwrap children located between break points.
@@ -506,7 +541,7 @@ import isIterable from '../../utils/isiterable.js';
 		}
 
 		// Break attributes at range start and end.
-		const { start: breakStart, end: breakEnd } = this.breakRange( range );
+		const { start: breakStart, end: breakEnd } = this._breakRange( range, true );
 		const parentContainer = breakStart.parent;
 
 		// Unwrap children located between break points.
@@ -657,8 +692,8 @@ function wrapChildren( writer, parent, startOffset, endOffset, attribute ) {
 // Returns new position that is moved to near text node. Returns same position if there is no text node before of after
 // specified position.
 //
-//		<p>{foo}|</p>  ->  <p>{foo|}</p>
-//		<p>|{foo}</p>  ->  <p>{|foo}</p>
+//		<p>foo[]</p>  ->  <p>foo{}</p>
+//		<p>[]foo</p>  ->  <p>{}foo</p>
 //
 // @private
 // @param {engine.treeView.Position} position
@@ -682,9 +717,9 @@ function movePositionToTextNode( position ) {
 
 // Breaks text node into two text nodes when possible.
 //
-//		<p>{foo|bar}</p> -> <p>{foo}|{bar}</p>
-//		<p>{|foobar}</p> -> <p>|{foobar}</p>
-//		<p>{foobar|}</p> -> <p>{foobar}|</p>
+//		<p>foo{}bar</p> -> <p>foo[]bar</p>
+//		<p>{}foobar</p> -> <p>[]foobar</p>
+//		<p>foobar{}</p> -> <p>foobar[]</p>
 //
 // @private
 // @param {engine.treeView.Position} position Position that need to be placed inside text node.

+ 151 - 74
packages/ckeditor5-engine/tests/_utils-tests/model.js

@@ -5,24 +5,94 @@
 
 'use strict';
 
-import { getData, setData } from '/tests/engine/_utils/model.js';
+import { stringify, parse, getData, setData } from '/tests/engine/_utils/model.js';
 import Document from '/ckeditor5/engine/treemodel/document.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
+import Position from '/ckeditor5/engine/treemodel/position.js';
 
 describe( 'model test utils', () => {
-	let document, root, selection;
+	let document, root, selection, sandbox;
 
 	beforeEach( () => {
 		document = new Document();
 		root = document.createRoot( 'main', '$root' );
 		selection = document.selection;
-
+		sandbox = sinon.sandbox.create();
 		selection.removeAllRanges();
 	} );
 
+	afterEach( () => {
+		sandbox.restore();
+	} );
+
 	describe( 'getData', () => {
+		it( 'should use stringify method', () => {
+			const stringifySpy = sandbox.spy( getData, '_stringify' );
+			root.appendChildren( new Element( 'b', null, [ 'btext' ] ) );
+
+			expect( getData( document, { withoutSelection: true } ) ).to.equal( '<b>btext</b>' );
+			sinon.assert.calledOnce( stringifySpy );
+			sinon.assert.calledWithExactly( stringifySpy, root );
+		} );
+
+		it( 'should use stringify method with selection', () => {
+			const stringifySpy = sandbox.spy( getData, '_stringify' );
+			root.appendChildren( new Element( 'b', null, [ 'btext' ] ) );
+			document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+
+			expect( getData( document ) ).to.equal( '<selection><b>btext</b></selection>' );
+			sinon.assert.calledOnce( stringifySpy );
+			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
+		} );
+	} );
+
+	describe( 'setData', () => {
+		it( 'should use parse method', () => {
+			const parseSpy = sandbox.spy( setData, '_parse' );
+			const options = {};
+			const data = '<b>btext</b>text';
+
+			setData( document, data, options );
+
+			expect( getData( document, { withoutSelection: true } ) ).to.equal( data );
+			sinon.assert.calledOnce( parseSpy );
+			const args = parseSpy.firstCall.args;
+			expect( args[ 0 ] ).to.equal( data );
+		} );
+
+		it( 'should use parse method with selection', () => {
+			const parseSpy = sandbox.spy( setData, '_parse' );
+			const options = {};
+			const data = '<selection><b>btext</b></selection>';
+
+			setData( document, data, options );
+
+			expect( getData( document ) ).to.equal( data );
+			sinon.assert.calledOnce( parseSpy );
+			const args = parseSpy.firstCall.args;
+			expect( args[ 0 ] ).to.equal( data );
+		} );
+	} );
+
+	describe( 'stringify', () => {
+		it( 'should stringify text', () => {
+			const text = new Text( 'text', { underline: true, bold: true } );
+			expect( stringify( text ) ).to.equal( '<$text bold=true underline=true>text</$text>' );
+		} );
+
+		it( 'should stringify element', () => {
+			const element = new Element( 'a', null, [ new Element( 'b', null, 'btext' ), 'atext' ] );
+			expect( stringify( element ) ).to.equal( '<a><b>btext</b>atext</a>' );
+		} );
+
+		it( 'should stringify document fragment', () => {
+			const fragment = new DocumentFragment( [ new Element( 'b', null, 'btext' ), 'atext' ] );
+			expect( stringify( fragment ) ).to.equal( '<b>btext</b>atext' );
+		} );
+
 		it( 'writes elements and texts', () => {
 			root.appendChildren( [
 				new Element( 'a', null, 'atext' ),
@@ -34,7 +104,7 @@ describe( 'model test utils', () => {
 				new Element( 'd' )
 			] );
 
-			expect( getData( document, 'main' ) ).to.equal(
+			expect( stringify( root ) ).to.equal(
 				'<a>atext</a><b><c1></c1>ctext<c2></c2></b><d></d>'
 			);
 		} );
@@ -48,7 +118,7 @@ describe( 'model test utils', () => {
 
 			// Note: attributes are written in a very simplistic way, because they are not to be parsed. They are just
 			// to be compared in the tests with some patterns.
-			expect( getData( document, 'main' ) ).to.equal(
+			expect( stringify( root ) ).to.equal(
 				'<a bar=1 car=false foo=true><b barFoo={"x":1,"y":2} fooBar="x y"></b></a>'
 			);
 		} );
@@ -63,7 +133,7 @@ describe( 'model test utils', () => {
 				] )
 			] );
 
-			expect( getData( document, 'main' ) ).to.equal(
+			expect( stringify( root ) ).to.equal(
 				'<$text bold=true>foo</$text>' +
 				'bar' +
 				'<$text bold=true italic=true>bom</$text>' +
@@ -71,9 +141,8 @@ describe( 'model test utils', () => {
 			);
 		} );
 
-		describe( 'options.selection', () => {
+		describe( 'selection', () => {
 			let elA, elB;
-			const options = { selection: true };
 
 			beforeEach( () => {
 				elA = new Element( 'a' );
@@ -89,10 +158,9 @@ describe( 'model test utils', () => {
 
 			it( 'writes selection in an empty root', () => {
 				const root = document.createRoot( 'empty', '$root' );
-
 				selection.collapse( root );
 
-				expect( getData( document, 'empty', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<selection />'
 				);
 			} );
@@ -100,7 +168,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed in an element', () => {
 				selection.collapse( root );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<selection /><a></a>foo<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -108,7 +176,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed in a text', () => {
 				selection.collapse( root, 3 );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>fo<selection />o<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -116,7 +184,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed at the text left boundary', () => {
 				selection.collapse( elA, 'AFTER' );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a><selection />foo<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -124,7 +192,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed at the text right boundary', () => {
 				selection.collapse( elB, 'BEFORE' );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold=true>bar</$text><selection bold=true /><b></b>'
 				);
 			} );
@@ -135,7 +203,7 @@ describe( 'model test utils', () => {
 				// Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
 				selection.clearAttributes();
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold=true>bar</$text><b></b><selection />'
 				);
 			} );
@@ -144,7 +212,7 @@ describe( 'model test utils', () => {
 				selection.collapse( root );
 				selection.setAttributesTo( { italic: true, bold: true } );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection )  ).to.equal(
 					'<selection bold=true italic=true /><a></a>foo<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -152,7 +220,7 @@ describe( 'model test utils', () => {
 			it( 'writes selection collapsed selection in a text with attributes', () => {
 				selection.collapse( root, 5 );
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold=true>b<selection bold=true />ar</$text><b></b>'
 				);
 			} );
@@ -162,7 +230,7 @@ describe( 'model test utils', () => {
 					Range.createFromParentsAndOffsets( root, 0, root, 4 )
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<selection><a></a>foo</selection><$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -172,7 +240,7 @@ describe( 'model test utils', () => {
 					Range.createFromParentsAndOffsets( root, 2, root, 3 )
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>f<selection>o</selection>o<$text bold=true>bar</$text><b></b>'
 				);
 			} );
@@ -182,7 +250,7 @@ describe( 'model test utils', () => {
 					Range.createFromParentsAndOffsets( elA, 0, elB, 0 )
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a><selection></a>foo<$text bold=true>bar</$text><b></selection></b>'
 				);
 			} );
@@ -193,14 +261,30 @@ describe( 'model test utils', () => {
 					true
 				);
 
-				expect( getData( document, 'main', options ) ).to.equal(
+				expect( stringify( root, selection ) ).to.equal(
 					'<a><selection backward></a>foo<$text bold=true>bar</$text><b></selection></b>'
 				);
 			} );
+
+			it( 'uses range and coverts it to selection', () => {
+				const range = Range.createFromParentsAndOffsets( elA, 0, elB, 0 );
+
+				expect( stringify( root, range ) ).to.equal(
+					'<a><selection></a>foo<$text bold=true>bar</$text><b></selection></b>'
+				);
+			} );
+
+			it( 'uses position and converts it to collapsed selection', () => {
+				const position = new Position( root, [ 0 ] );
+
+				expect( stringify( root, position ) ).to.equal(
+					'<selection /><a></a>foo<$text bold=true>bar</$text><b></b>'
+				);
+			} );
 		} );
 	} );
 
-	describe( 'setData', () => {
+	describe( 'parse', () => {
 		test( 'creates elements', {
 			data: '<a></a><b><c></c></b>'
 		} );
@@ -212,21 +296,21 @@ describe( 'model test utils', () => {
 		test( 'sets elements attributes', {
 			data: '<a foo=1 bar=true car="x y"><b x="y"></b></a>',
 			output: '<a bar=true car="x y" foo=1><b x="y"></b></a>',
-			check() {
+			check( root ) {
 				expect( root.getChild( 0 ).getAttribute( 'car' ) ).to.equal( 'x y' );
 			}
 		} );
 
 		test( 'sets complex attributes', {
 			data: '<a foo={"a":1,"b":"c"}></a>',
-			check() {
+			check( root ) {
 				expect( root.getChild( 0 ).getAttribute( 'foo' ) ).to.have.property( 'a', 1 );
 			}
 		} );
 
 		test( 'sets text attributes', {
 			data: '<$text bold=true italic=true>foo</$text><$text bold=true>bar</$text>bom',
-			check() {
+			check( root ) {
 				expect( root.getChildCount() ).to.equal( 9 );
 				expect( root.getChild( 0 ) ).to.have.property( 'character', 'f' );
 				expect( root.getChild( 0 ).getAttribute( 'italic' ) ).to.equal( true );
@@ -235,139 +319,132 @@ describe( 'model test utils', () => {
 
 		it( 'throws when unexpected closing tag', () => {
 			expect( () => {
-				setData( document, 'main', '<a><b></a></b>' );
-			} ).to.throw();
+				parse( '<a><b></a></b>' );
+			} ).to.throw( Error, 'Parse error - unexpected closing tag.' );
 		} );
 
 		it( 'throws when unexpected attribute', () => {
 			expect( () => {
-				setData( document, 'main', '<a ?></a>' );
-			} ).to.throw();
+				parse( '<a ?></a>' );
+			} ).to.throw( Error, 'Parse error - unexpected token: ?.' );
 		} );
 
 		it( 'throws when incorrect tag', () => {
 			expect( () => {
-				setData( document, 'main', '<a' );
-			} ).to.throw();
+				parse( '<a' );
+			} ).to.throw( Error, 'Parse error - unexpected token: <a.' );
 		} );
 
 		it( 'throws when missing closing tag', () => {
 			expect( () => {
-				setData( document, 'main', '<a><b></b>' );
-			} ).to.throw();
+				parse( '<a><b></b>' );
+			} ).to.throw( Error, 'Parse error - missing closing tags: a.' );
 		} );
 
 		it( 'throws when missing opening tag for text', () => {
 			expect( () => {
-				setData( document, 'main', '</$text>' );
-			} ).to.throw();
+				parse( '</$text>' );
+			} ).to.throw( Error, 'Parse error - unexpected closing tag.' );
 		} );
 
 		it( 'throws when missing closing tag for text', () => {
 			expect( () => {
-				setData( document, 'main', '<$text>' );
-			} ).to.throw();
+				parse( '<$text>' );
+			} ).to.throw( Error, 'Parse error - missing closing tags: $text.' );
 		} );
 
 		describe( 'selection', () => {
-			const getDataOptions = { selection: true };
-
 			test( 'sets collapsed selection in an element', {
 				data: '<a><selection /></a>',
-				getDataOptions,
-				check() {
-					expect( document.selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
+				check( root, selection ) {
+					expect( selection.getFirstPosition().parent ).to.have.property( 'name', 'a' );
 				}
 			} );
 
 			test( 'sets collapsed selection between elements', {
-				data: '<a></a><selection /><b></b>',
-				getDataOptions
+				data: '<a></a><selection /><b></b>'
 			} );
 
 			test( 'sets collapsed selection before a text', {
-				data: '<a></a><selection />foo',
-				getDataOptions
+				data: '<a></a><selection />foo'
 			} );
 
 			test( 'sets collapsed selection after a text', {
-				data: 'foo<selection />',
-				getDataOptions
+				data: 'foo<selection />'
 			} );
 
 			test( 'sets collapsed selection within a text', {
 				data: 'foo<selection />bar',
-				getDataOptions,
-				check() {
+				check( root ) {
 					expect( root.getChildCount() ).to.equal( 6 );
 				}
 			} );
 
 			test( 'sets selection attributes', {
 				data: 'foo<selection bold=true italic=true />bar',
-				getDataOptions,
-				check() {
+				check( root, selection ) {
 					expect( selection.getAttribute( 'italic' ) ).to.be.true;
 				}
 			} );
 
 			test( 'sets collapsed selection between text and text with attributes', {
 				data: 'foo<selection /><$text bold=true>bar</$text>',
-				getDataOptions,
-				check() {
+				check( root, selection ) {
 					expect( root.getChildCount() ).to.equal( 6 );
-					expect( document.selection.getAttribute( 'bold' ) ).to.be.undefined;
+					expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
 				}
 			} );
 
 			test( 'sets selection containing an element', {
-				data: 'x<selection><a></a></selection>',
-				getDataOptions
+				data: 'x<selection><a></a></selection>'
 			} );
 
 			test( 'sets selection with attribute containing an element', {
-				data: 'x<selection bold=true><a></a></selection>',
-				getDataOptions
+				data: 'x<selection bold=true><a></a></selection>'
 			} );
 
 			test( 'sets a backward selection containing an element', {
-				data: 'x<selection backward bold=true><a></a></selection>',
-				getDataOptions
+				data: 'x<selection backward bold=true><a></a></selection>'
 			} );
 
 			test( 'sets selection within a text', {
-				data: 'x<selection bold=true>y</selection>z',
-				getDataOptions
+				data: 'x<selection bold=true>y</selection>z'
 			} );
 
 			test( 'sets selection within a text with different attributes', {
-				data: '<$text bold=true>fo<selection bold=true>o</$text>ba</selection>r',
-				getDataOptions
+				data: '<$text bold=true>fo<selection bold=true>o</$text>ba</selection>r'
 			} );
 
 			it( 'throws when missing selection start', () => {
 				expect( () => {
-					setData( document, 'main', 'foo</selection>' );
-				} ).to.throw();
+					parse( 'foo</selection>' );
+				} ).to.throw( Error, 'Parse error - missing selection start.' );
 			} );
 
 			it( 'throws when missing selection end', () => {
 				expect( () => {
-					setData( document, 'main', '<selection>foo' );
-				} ).to.throw();
+					parse( '<selection>foo' );
+				} ).to.throw( Error, 'Parse error - missing selection end.' );
 			} );
 		} );
 
 		function test( title, options ) {
 			it( title, () => {
-				let output = options.output || options.data;
-
-				setData( document, 'main', options.data, options.setDataOptions );
+				const output = options.output || options.data;
+				const data = parse( options.data );
+				let model, selection;
+
+				if ( data.selection && data.model ) {
+					model = data.model;
+					selection = data.selection;
+				} else {
+					model = data;
+				}
 
-				expect( getData( document, 'main', options.getDataOptions ) ).to.equal( output );
+				expect( stringify( model, selection ) ).to.equal( output );
 
 				if ( options.check ) {
-					options.check();
+					options.check( model, selection );
 				}
 			} );
 		}

+ 104 - 2
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -5,7 +5,9 @@
 
 'use strict';
 
-import { stringify, parse } from '/tests/engine/_utils/view.js';
+/* bender-tags: browser-only */
+
+import { parse, stringify, getData, setData }from '/tests/engine/_utils/view.js';
 import DocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
 import Position from '/ckeditor5/engine/treeview/position.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
@@ -14,8 +16,94 @@ import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
 import Selection from '/ckeditor5/engine/treeview/selection.js';
 import Range from '/ckeditor5/engine/treeview/range.js';
+import Document from '/ckeditor5/engine/treeview/document.js';
 
 describe( 'view test utils', () => {
+	describe( 'getData, setData', () => {
+		let sandbox;
+
+		beforeEach( () => {
+			sandbox = sinon.sandbox.create();
+		} );
+
+		afterEach( () => {
+			sandbox.restore();
+		} );
+
+		describe( 'getData', () => {
+			it( 'should use stringify method', () => {
+				const element = document.createElement( 'div' );
+				const stringifySpy = sandbox.spy( getData, '_stringify' );
+				const viewDocument = new Document();
+				const options = { showType: false, showPriority: false, withoutSelection: true };
+				const root = viewDocument.createRoot( element );
+				root.appendChildren( new Element( 'p' ) );
+
+				expect( getData( viewDocument, options ) ).to.equal( '<p></p>' );
+				sinon.assert.calledOnce( stringifySpy );
+				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
+				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( null );
+				const stringifyOptions = stringifySpy.firstCall.args[ 2 ];
+				expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
+			} );
+
+			it( 'should use stringify method with selection', () => {
+				const element = document.createElement( 'div' );
+				const stringifySpy = sandbox.spy( getData, '_stringify' );
+				const viewDocument = new Document();
+				const options = { showType: false, showPriority: false };
+				const root = viewDocument.createRoot( element );
+				root.appendChildren( new Element( 'p' ) );
+
+				viewDocument.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+
+				expect( getData( viewDocument, options ) ).to.equal( '[<p></p>]' );
+				sinon.assert.calledOnce( stringifySpy );
+				expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root );
+				expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( viewDocument.selection );
+				const stringifyOptions = stringifySpy.firstCall.args[ 2 ];
+				expect( stringifyOptions ).to.have.property( 'showType' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false );
+				expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true );
+			} );
+		} );
+
+		describe( 'setData', () => {
+			it( 'should use parse method', () => {
+				const viewDocument = new Document();
+				const data = 'foobar<b>baz</b>';
+				const parseSpy = sandbox.spy( setData, '_parse' );
+
+				viewDocument.createRoot( document.createElement( 'div' ) );
+				setData( viewDocument, data );
+
+				expect( getData( viewDocument ) ).to.equal( 'foobar<b>baz</b>' );
+				sinon.assert.calledOnce( parseSpy );
+				const args = parseSpy.firstCall.args;
+				expect( args[ 0 ] ).to.equal( data );
+				expect( args[ 1 ] ).to.be.an( 'object' );
+				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
+			} );
+
+			it( 'should use parse method with selection', () => {
+				const viewDocument = new Document();
+				const data = '[<b>baz</b>]';
+				const parseSpy = sandbox.spy( setData, '_parse' );
+
+				viewDocument.createRoot( document.createElement( 'div' ) );
+				setData( viewDocument, data );
+
+				expect( getData( viewDocument ) ).to.equal( '[<b>baz</b>]' );
+				const args = parseSpy.firstCall.args;
+				expect( args[ 0 ] ).to.equal( data );
+				expect( args[ 1 ] ).to.be.an( 'object' );
+				expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() );
+			} );
+		} );
+	} );
+
 	describe( 'stringify', () => {
 		it( 'should write text', () => {
 			const text = new Text( 'foobar' );
@@ -433,5 +521,19 @@ describe( 'view test utils', () => {
 				parse( '<container:b:10:test></container:b:10:test>' );
 			} ).to.throw( Error );
 		} );
+
+		it( 'should use provided root element #1', () => {
+			const root = new Element( 'p' );
+			const data = parse( '<span>text</span>', { rootElement: root } );
+
+			expect( stringify( data ) ).to.equal( '<p><span>text</span></p>' );
+		} );
+
+		it( 'should use provided root element #2', () => {
+			const root = new Element( 'p' );
+			const data = parse( '<span>text</span><b>test</b>', { rootElement: root } );
+
+			expect( stringify( data ) ).to.equal( '<p><span>text</span><b>test</b></p>' );
+		} );
 	} );
-} );
+} );

+ 122 - 27
packages/ckeditor5-engine/tests/_utils/model.js

@@ -9,39 +9,106 @@ import TreeWalker from '/ckeditor5/engine/treemodel/treewalker.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
+import RootElement from '/ckeditor5/engine/treemodel/rootelement.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
+import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
+import Selection from '/ckeditor5/engine/treemodel/selection.js';
+import Document from '/ckeditor5/engine/treemodel/document.js';
 
 /**
- * Writes the contents of the document to an HTML-like string.
+ * Writes the contents of the {@link engine.treeModel.Document Document} to an HTML-like string.
  *
  * @param {engine.treeModel.Document} document
- * @param {String} rootName
  * @param {Object} [options]
- * @param {Boolean} [options.selection] Whether to write the selection.
+ * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
+ * be not included in returned string.
+ * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
+ * default `main` name will be used.
  * @returns {String} The stringified data.
  */
-export function getData( document, rootName, options ) {
+export function getData( document, options = {} ) {
+	const withoutSelection = !!options.withoutSelection;
+	const rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
+
+	return withoutSelection ? getData._stringify( root ) : getData._stringify( root, document.selection );
+}
+
+// Set stringify as getData private method - needed for testing/spying.
+getData._stringify = stringify;
+
+/**
+ * Sets the contents of the {@link engine.treeModel.Document Document} provided as HTML-like string.
+ *
+ * @param {engine.treeModel.Document} document
+ * @param {String} data HTML-like string to write into Document.
+ * @param {Object} options
+ * @param {String} [options.rootName] Root name where parsed data will be stored. If not provided, default `main` name will be
+ * used.
+ */
+export function setData( document, data, options = {} ) {
+	setData._parse( data, {
+		document: document,
+		rootName: options.rootName
+	} );
+}
+
+// Set parse as setData private method - needed for testing/spying.
+setData._parse = parse;
+
+/**
+ * Converts model nodes to HTML-like string representation.
+ *
+ * @param {engine.treeModel.RootElement|engine.treeModel.Element|engine.treeModel.Text|
+ * engine.treeModel.DocumentFragment} node Node to stringify.
+ * @param {engine.treeModel.Selection|engine.treeModel.Position|engine.treeModel.Range} [selectionOrPositionOrRange = null ]
+ * Selection instance which ranges will be included in returned string data. If Range instance is provided - it will be
+ * converted to selection containing this range. If Position instance is provided - it will be converted to selection
+ * containing one range collapsed at this position.
+ * @returns {String} HTML-like string representing the model.
+ */
+export function stringify( node, selectionOrPositionOrRange = null ) {
+	let selection;
+	let document;
+
+	if ( node instanceof RootElement ) {
+		document = node.document;
+	} else if ( node instanceof Element || node instanceof Text ) {
+		// If root is Element or Text - wrap it with DocumentFragment.
+		node = new DocumentFragment( node );
+	}
+
+	document = document || new Document();
+
 	const walker = new TreeWalker( {
-		boundaries: Range.createFromElement( root )
+		boundaries: Range.createFromElement( node )
 	} );
-	let ret = '';
-	let lastPosition = Position.createFromParentAndOffset( root, 0 );
-	const selection = document.selection;
 
-	options = options || {};
+	if ( selectionOrPositionOrRange instanceof Selection ) {
+		selection = selectionOrPositionOrRange;
+	} else if ( selectionOrPositionOrRange instanceof Range ) {
+		selection = document.selection;
+		selection.addRange( selectionOrPositionOrRange );
+	} else if ( selectionOrPositionOrRange instanceof Position ) {
+		selection = document.selection;
+		selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
+	}
+
+	let ret = '';
+	let lastPosition = Position.createFromParentAndOffset( node, 0 );
+	const withSelection = !!selection;
 
 	for ( let value of walker ) {
-		if ( options.selection ) {
+		if ( withSelection ) {
 			ret += writeSelection( value.previousPosition, selection );
 		}
 
-		ret += writeItem( value, selection, options );
+		ret += writeItem( value, selection, { selection: withSelection } );
 
 		lastPosition = value.nextPosition;
 	}
 
-	if ( options.selection ) {
+	if ( withSelection ) {
 		ret += writeSelection( lastPosition, selection );
 	}
 
@@ -49,20 +116,37 @@ export function getData( document, rootName, options ) {
 }
 
 /**
- * Sets the contents of the model and the selection in it.
+ * Parses HTML-like string and returns model {@link engine.treeModel.RootElement rootElement}.
  *
- * @param {engine.treeModel.Document} document
- * @param {String} rootName
- * @param {String} data
+ * @param {String} data HTML-like string to be parsed.
+ * @param {Object} options
+ * @param {engine.treeModel.Document} [options.document] Document from which root element and selection will be used. If
+ * not provided new {engine.treeModel.Document document} instance will be created.
+ * @param {String} [options.rootName='main'] When `document` option is provided this root name will be used to create
+ * {engine.treeModel.RootElement RootElement} instance.
+ * @returns {engine.treeModel.RootElement|Object} Returns parsed RootElement or object with two fields `model`
+ * and `selection` when selection ranges were included in data to parse.
  */
-export function setData( document, rootName, data ) {
-	let appendTo = document.getRoot( rootName );
+export function parse( data, options = {} ) {
+	let document, root;
+	let withSelection = false;
+	const rootName = options.rootName || 'main';
+
+	if ( options.document ) {
+		document = options.document;
+		root = document.getRoot( rootName );
+		root.removeChildren( 0, root.getChildCount() );
+	} else {
+		document = new Document();
+		root = document.createRoot( rootName );
+	}
+
 	const path = [];
 	let selectionStart, selectionEnd, selectionAttributes, textAttributes;
 
 	const handlers = {
 		text( token ) {
-			appendTo.appendChildren( new Text( token.text, textAttributes ) );
+			root.appendChildren( new Text( token.text, textAttributes ) );
 		},
 
 		textStart( token ) {
@@ -80,9 +164,9 @@ export function setData( document, rootName, data ) {
 
 		openingTag( token ) {
 			let el = new Element( token.name, token.attributes );
-			appendTo.appendChildren( el );
+			root.appendChildren( el );
 
-			appendTo = el;
+			root = el;
 
 			path.push( token.name );
 		},
@@ -92,25 +176,27 @@ export function setData( document, rootName, data ) {
 				throw new Error( 'Parse error - unexpected closing tag.' );
 			}
 
-			appendTo = appendTo.parent;
+			root = root.parent;
 		},
 
 		collapsedSelection( token ) {
-			document.selection.collapse( appendTo, 'END' );
+			withSelection = true;
+			document.selection.collapse( root, 'END' );
 			document.selection.setAttributesTo( token.attributes );
 		},
 
 		selectionStart( token ) {
-			selectionStart = Position.createFromParentAndOffset( appendTo, appendTo.getChildCount() );
+			selectionStart = Position.createFromParentAndOffset( root, root.getChildCount() );
 			selectionAttributes = token.attributes;
 		},
 
 		selectionEnd() {
 			if ( !selectionStart ) {
-				throw new Error( 'Parse error - missing selection start' );
+				throw new Error( 'Parse error - missing selection start.' );
 			}
 
-			selectionEnd = Position.createFromParentAndOffset( appendTo, appendTo.getChildCount() );
+			withSelection = true;
+			selectionEnd = Position.createFromParentAndOffset( root, root.getChildCount() );
 
 			document.selection.setRanges(
 				[ new Range( selectionStart, selectionEnd ) ],
@@ -134,6 +220,15 @@ export function setData( document, rootName, data ) {
 	if ( selectionStart && !selectionEnd ) {
 		throw new Error( 'Parse error - missing selection end.' );
 	}
+
+	if ( withSelection ) {
+		return {
+			model: root,
+			selection: document.selection
+		};
+	}
+
+	return root;
 }
 
 // -- getData helpers ---------------------------------------------------------
@@ -324,7 +419,7 @@ function consumeNextToken( data ) {
 		}
 	}
 
-	throw new Error( 'Parse error - unpexpected token: ' + data + '.' );
+	throw new Error( 'Parse error - unexpected token: ' + data + '.' );
 }
 
 function parseAttributes( attrsString ) {

+ 94 - 17
packages/ckeditor5-engine/tests/_utils/view.js

@@ -15,16 +15,68 @@ import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import ViewText from '/ckeditor5/engine/treeview/text.js';
 
-const DomDocumentFragment = window.DocumentFragment;
-const DomElement = window.Element;
-
 const ELEMENT_RANGE_START_TOKEN = '[';
 const ELEMENT_RANGE_END_TOKEN = ']';
 const TEXT_RANGE_START_TOKEN = '{';
 const TEXT_RANGE_END_TOKEN = '}';
 
 /**
- * Converts view elements to its HTML-like string representation.
+ * Writes the contents of the {@link engine.treeView.Document Document} to an HTML-like string.
+ *
+ * @param {engine.treeView.Document} document
+ * @param {Object} [options]
+ * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
+ * be not included in returned string.
+ * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
+ * default `main` name will be used.
+ * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
+ * instead of `<p>` and `<attribute:b>` instead of `<b>`).
+ * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
+ * (`<span:12>`, `<b:10>`).
+ * @returns {String} The stringified data.
+ */
+export function getData( document, options = {} ) {
+	const withoutSelection = !!options.withoutSelection;
+	const rootName = options.rootName || 'main';
+	const root = document.getRoot( rootName );
+	const stringifyOptions = {
+		showType: options.showType,
+		showPriority: options.showPriority,
+		ignoreRoot: true
+	};
+
+	return withoutSelection ?
+		getData._stringify( root, null, stringifyOptions ) :
+		getData._stringify( root, document.selection, stringifyOptions );
+}
+
+// Set stringify as getData private method - needed for testing/spying.
+getData._stringify = stringify;
+
+/**
+ * Sets the contents of the {@link engine.treeView.Document Document} provided as HTML-like string.
+ *
+ * @param {engine.treeView.Document} document
+ * @param {String} data HTML-like string to write into Document.
+ * @param {Object} options
+ * @param {String} [rootName] Root name where parsed data will be stored. If not provided, default `main` name will be
+ * used.
+ */
+export function setData( document, data, options = {} ) {
+	const rootName = options.rootName || 'main';
+	const root = document.getRoot( rootName );
+	const result = setData._parse( data, { rootElement: root } );
+
+	if ( result.view && result.selection ) {
+		document.selection.setTo( result.selection );
+	}
+}
+
+// Set parse as setData private method - needed for testing/spying.
+setData._parse = parse;
+
+/**
+ * Converts view elements to HTML-like string representation.
  * Root element can be provided as {@link engine.treeView.Text Text}:
  *
  *		const text = new Text( 'foobar' );
@@ -114,6 +166,8 @@ const TEXT_RANGE_END_TOKEN = '}';
  * instead of `<p>` and `<attribute:b>` instead of `<b>`).
  * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
  * (`<span:12>`, `<b:10>`).
+ * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing will not be printed.
+ * Mainly used by `getData` function to ignore {@link engine.treeView.Document Document's} root element.
  * @returns {String} HTML-like string representing the view.
  */
 export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
@@ -123,7 +177,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
 		selection = new Selection();
 		selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
 	} else if ( selectionOrPositionOrRange instanceof Range ) {
-		selection = new Selection( );
+		selection = new Selection();
 		selection.addRange( selectionOrPositionOrRange );
 	} else {
 		selection = selectionOrPositionOrRange;
@@ -179,15 +233,19 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  * selection instance. For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
  * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward to the returned
  * {@link engine.treeView.Selection Selection} instance.
+ * @param {engine.treeView.Element|engine.treeView.DocumentFragment} [options.rootElement=null] Default root to use when parsing elements.
+ * When set to `null` root element will be created automatically. If set to
+ * {@link engine.treeView.Element Element} or {@link engine.treeView.DocumentFragment DocumentFragment} - this node
+ * will be used as root for all parsed nodes.
  * @returns {engine.treeView.Text|engine.treeView.Element|engine.treeView.DocumentFragment|Object} Returns parsed view node
  * or object with two fields `view` and `selection` when selection ranges were included in data to parse.
  */
-export function parse( data, options = { } ) {
+export function parse( data, options = {} ) {
 	options.order = options.order || [];
 	const viewParser = new ViewParser();
 	const rangeParser = new RangeParser();
 
-	const view = viewParser.parse( data );
+	const view = viewParser.parse( data, options.rootElement );
 	const ranges = rangeParser.parse( view, options.order );
 
 	// When ranges are present - return object containing view, and selection.
@@ -409,16 +467,25 @@ class ViewParser {
 	 * Parses HTML-like string to view tree elements.
 	 *
 	 * @param {String} data
+	 * @param {engine.treeView.Element|engine.treeView.DocumentFragment} [rootElement=null] Default root to use when parsing elements.
+	 * When set to `null` root element will be created automatically. If set to
+	 * {@link engine.treeView.Element Element} or {@link engine.treeView.DocumentFragment DocumentFragment} - this node
+	 * will be used as root for all parsed nodes.
 	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
 	 */
-	parse( data ) {
+	parse( data, rootElement = null ) {
 		const htmlProcessor = new HtmlDataProcessor();
 
 		// Convert HTML string to DOM.
 		const domRoot = htmlProcessor.toDom( data );
 
+		// If root element is provided - remove all children from it.
+		if ( rootElement ) {
+			rootElement.removeChildren( 0, rootElement.getChildCount() );
+		}
+
 		// Convert DOM to View.
-		return this._walkDom( domRoot );
+		return this._walkDom( domRoot, rootElement );
 	}
 
 	/**
@@ -426,26 +493,32 @@ class ViewParser {
 	 *
 	 * @private
 	 * @param {Node} domNode
+	 * @param {engine.treeView.Element|engine.treeView.DocumentFragment} [rootElement=null] Default root element to use
+	 * when parsing elements.
 	 * @returns {engine.treeView.Node|engine.treeView.DocumentFragment}
 	 */
-	_walkDom( domNode ) {
-		const isDomElement = domNode instanceof DomElement;
+	_walkDom( domNode, rootElement = null ) {
+		const isDomElement = domNode instanceof window.Element;
 
-		if ( isDomElement || domNode instanceof DomDocumentFragment ) {
+		if ( isDomElement || domNode instanceof window.DocumentFragment ) {
 			const children = domNode.childNodes;
 			const length = children.length;
 
 			// If there is only one element inside DOM DocumentFragment - use it as root.
 			if ( !isDomElement && length == 1 ) {
-				return this._walkDom( domNode.childNodes[ 0 ] );
+				return this._walkDom( domNode.childNodes[ 0 ], rootElement );
 			}
 
 			let viewElement;
 
 			if ( isDomElement ) {
 				viewElement = this._convertElement( domNode );
+
+				if ( rootElement ) {
+					rootElement.appendChildren( viewElement );
+				}
 			} else {
-				viewElement = new ViewDocumentFragment();
+				viewElement = rootElement ? rootElement : new ViewDocumentFragment();
 			}
 
 			for ( let i = 0; i < children.length; i++ ) {
@@ -453,7 +526,7 @@ class ViewParser {
 				viewElement.appendChildren( this._walkDom( child ) );
 			}
 
-			return viewElement;
+			return rootElement ? rootElement : viewElement;
 		}
 
 		return new ViewText( domNode.textContent );
@@ -609,6 +682,8 @@ class ViewStringify {
 	 * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed ( `<container:p>`
 	 * instead of `<p>` and `<attribute:b>` instead of `<b>`.
 	 * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
+	 * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing tag will not
+	 * be outputted.
 	 */
 	constructor( root, selection = null, options = {} ) {
 		this.root = root;
@@ -621,6 +696,7 @@ class ViewStringify {
 
 		this.showType = !!options.showType;
 		this.showPriority = !!options.showPriority;
+		this.ignoreRoot = !!options.ignoreRoot;
 	}
 
 	/**
@@ -647,9 +723,10 @@ class ViewStringify {
 	 */
 	_walkView( root, callback ) {
 		const isElement = root instanceof ViewElement;
+		const ignore = this.ignoreRoot && this.root === root;
 
 		if ( isElement || root instanceof ViewDocumentFragment ) {
-			if ( isElement ) {
+			if ( isElement && !ignore ) {
 				callback( this._stringifyElementOpen( root ) );
 			}
 
@@ -662,7 +739,7 @@ class ViewStringify {
 				callback( this._stringifyElementRanges( root, offset ) );
 			}
 
-			if ( isElement ) {
+			if ( isElement && !ignore ) {
 				callback( this._stringifyElementClose( root ) );
 			}
 		}

+ 2 - 0
packages/ckeditor5-engine/tests/dataprocessor/basichtmlwriter.js

@@ -3,6 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
+/* bender-tags: browser-only */
+
 'use strict';
 
 import BasicHtmlWriter from '/ckeditor5/engine/dataprocessor/basichtmlwriter.js';

+ 3 - 1
packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js

@@ -3,10 +3,12 @@
  * For licensing, see LICENSE.md.
  */
 
+/* bender-tags: browser-only */
+
 'use strict';
 
 import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
-import xssTemplates from '_utils/xsstemplates.js';
+import xssTemplates from './_utils/xsstemplates.js';
 
 describe( 'HtmlDataProcessor', () => {
 	const dataProcessor = new HtmlDataProcessor();

+ 17 - 17
packages/ckeditor5-engine/tests/treecontroller/mapper.js

@@ -194,23 +194,23 @@ describe( 'Mapper', () => {
 			expect( viewPosition.offset ).to.equal( viewOffset );
 		}
 
-		it( 'should transform modelDiv 0', () => createToViewTest( modelDiv, 0, viewDiv, 0 ) );
-		it( 'should transform modelDiv 1', () => createToViewTest( modelDiv, 1, viewDiv, 1 ) );
-		it( 'should transform modelDiv 2', () => createToViewTest( modelDiv, 2, viewDiv, 2 ) );
+		it( 'should transform modelDiv 0', () => createToViewTest( modelDiv, 0, viewTextX, 0 ) );
+		it( 'should transform modelDiv 1', () => createToViewTest( modelDiv, 1, viewTextX, 1 ) );
+		it( 'should transform modelDiv 2', () => createToViewTest( modelDiv, 2, viewTextZZ, 0 ) );
 		it( 'should transform modelDiv 3', () => createToViewTest( modelDiv, 3, viewTextZZ, 1 ) );
-		it( 'should transform modelDiv 4', () => createToViewTest( modelDiv, 4, viewDiv, 3 ) );
+		it( 'should transform modelDiv 4', () => createToViewTest( modelDiv, 4, viewTextZZ, 2 ) );
 
-		it( 'should transform modelP 0', () => createToViewTest( modelP, 0, viewP, 0 ) );
-		it( 'should transform modelP 1', () => createToViewTest( modelP, 1, viewP, 1 ) );
+		it( 'should transform modelP 0', () => createToViewTest( modelP, 0, viewTextY, 0 ) );
+		it( 'should transform modelP 1', () => createToViewTest( modelP, 1, viewTextY, 1 ) );
 		it( 'should transform modelP 2', () => createToViewTest( modelP, 2, viewTextFOO, 1 ) );
 		it( 'should transform modelP 3', () => createToViewTest( modelP, 3, viewTextFOO, 2 ) );
-		it( 'should transform modelP 4', () => createToViewTest( modelP, 4, viewP, 2 ) );
+		it( 'should transform modelP 4', () => createToViewTest( modelP, 4, viewTextBAR, 0 ) );
 		it( 'should transform modelP 5', () => createToViewTest( modelP, 5, viewTextBAR, 1 ) );
 		it( 'should transform modelP 6', () => createToViewTest( modelP, 6, viewTextBAR, 2 ) );
-		it( 'should transform modelP 7', () => createToViewTest( modelP, 7, viewP, 3 ) );
+		it( 'should transform modelP 7', () => createToViewTest( modelP, 7, viewTextBAR, 3 ) );
 		it( 'should transform modelP 8', () => createToViewTest( modelP, 8, viewP, 4 ) );
-		it( 'should transform modelP 9', () => createToViewTest( modelP, 9, viewU, 1 ) );
-		it( 'should transform modelP 10', () => createToViewTest( modelP, 10, viewU, 2 ) );
+		it( 'should transform modelP 9', () => createToViewTest( modelP, 9, viewTextB, 1 ) );
+		it( 'should transform modelP 10', () => createToViewTest( modelP, 10, viewTextM, 0 ) );
 		it( 'should transform modelP 11', () => createToViewTest( modelP, 11, viewP, 5 ) );
 	} );
 
@@ -229,7 +229,7 @@ describe( 'Mapper', () => {
 		it( 'should transform range', () => {
 			const modelRange = ModelRange.createFromParentsAndOffsets( modelDiv, 0, modelP, 3 );
 			const viewRange = mapper.toViewRange( modelRange );
-			expect( viewRange.start.parent ).to.equal( viewDiv );
+			expect( viewRange.start.parent ).to.equal( viewTextX );
 			expect( viewRange.start.offset ).to.equal( 0 );
 			expect( viewRange.end.parent ).to.equal( viewTextFOO );
 			expect( viewRange.end.offset ).to.equal( 2 );
@@ -354,17 +354,17 @@ describe( 'Mapper for widget', () => {
 			expect( viewPosition.offset ).to.equal( viewOffset );
 		}
 
-		it( 'should transform modelDiv 0', () => createToViewTest( modelDiv, 0, viewDiv, 0 ) );
-		it( 'should transform modelDiv 1', () => createToViewTest( modelDiv, 1, viewDiv, 1 ) );
-		it( 'should transform modelDiv 2', () => createToViewTest( modelDiv, 2, viewDiv, 2 ) );
+		it( 'should transform modelDiv 0', () => createToViewTest( modelDiv, 0, viewTextX, 0 ) );
+		it( 'should transform modelDiv 1', () => createToViewTest( modelDiv, 1, viewTextX, 1 ) );
+		it( 'should transform modelDiv 2', () => createToViewTest( modelDiv, 2, viewTextZZ, 0 ) );
 		it( 'should transform modelDiv 3', () => createToViewTest( modelDiv, 3, viewTextZZ, 1 ) );
-		it( 'should transform modelDiv 4', () => createToViewTest( modelDiv, 4, viewDiv, 3 ) );
+		it( 'should transform modelDiv 4', () => createToViewTest( modelDiv, 4, viewTextZZ, 2 ) );
 
 		it( 'should transform modelImg 0', () => createToViewTest( modelImg, 0, viewImg, 0 ) );
 
-		it( 'should transform modelCaption 0', () => createToViewTest( modelCaption, 0, viewCaption, 0 ) );
+		it( 'should transform modelCaption 0', () => createToViewTest( modelCaption, 0, viewTextFOO, 0 ) );
 		it( 'should transform modelCaption 1', () => createToViewTest( modelCaption, 1, viewTextFOO, 1 ) );
 		it( 'should transform modelCaption 2', () => createToViewTest( modelCaption, 2, viewTextFOO, 2 ) );
-		it( 'should transform modelCaption 3', () => createToViewTest( modelCaption, 3, viewCaption, 1 ) );
+		it( 'should transform modelCaption 3', () => createToViewTest( modelCaption, 3, viewTextFOO, 3 ) );
 	} );
 } );

+ 73 - 2
packages/ckeditor5-engine/tests/treecontroller/model-converter-builder.js

@@ -13,12 +13,14 @@ import ModelDocument from '/ckeditor5/engine/treemodel/document.js';
 import ModelElement from '/ckeditor5/engine/treemodel/element.js';
 import ModelText from '/ckeditor5/engine/treemodel/text.js';
 import ModelRange from '/ckeditor5/engine/treemodel/range.js';
+import ModelPosition from '/ckeditor5/engine/treemodel/position.js';
 
 import ViewElement from '/ckeditor5/engine/treeview/element.js';
 import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import ViewText from '/ckeditor5/engine/treeview/text.js';
 import ViewWriter from '/ckeditor5/engine/treeview/writer.js';
+import ViewSelection from '/ckeditor5/engine/treeview/selection.js';
 
 import Mapper from '/ckeditor5/engine/treecontroller/mapper.js';
 import ModelConversionDispatcher from '/ckeditor5/engine/treecontroller/modelconversiondispatcher.js';
@@ -29,6 +31,11 @@ import {
 	remove
 } from '/ckeditor5/engine/treecontroller/model-to-view-converters.js';
 
+import {
+	convertCollapsedSelection,
+	clearAttributes
+} from '/ckeditor5/engine/treecontroller/model-selection-to-view-converters.js';
+
 function viewAttributesToString( item ) {
 	let result = '';
 
@@ -63,7 +70,7 @@ function viewToString( item ) {
 }
 
 describe( 'Model converter builder', () => {
-	let dispatcher, modelDoc, modelRoot, viewRoot, mapper, writer;
+	let dispatcher, modelDoc, modelRoot, viewRoot, mapper, writer, viewSelection;
 
 	beforeEach( () => {
 		modelDoc = new ModelDocument();
@@ -75,8 +82,9 @@ describe( 'Model converter builder', () => {
 		mapper.bindElements( modelRoot, viewRoot );
 
 		writer = new ViewWriter();
+		viewSelection = new ViewSelection();
 
-		dispatcher = new ModelConversionDispatcher( { writer, mapper } );
+		dispatcher = new ModelConversionDispatcher( { writer, mapper, viewSelection } );
 
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'move', move() );
@@ -189,6 +197,69 @@ describe( 'Model converter builder', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div>foo</div>' );
 		} );
+
+		it( 'selection conversion', () => {
+			// This test requires collapsed range selection converter (breaking attributes)  and clearing "artifacts".
+			dispatcher.on( 'selection', clearAttributes() );
+			dispatcher.on( 'selection', convertCollapsedSelection() );
+
+			// Model converter builder should add selection converter.
+			BuildModelConverterFor( dispatcher ).fromAttribute( 'italic' ).toElement( ( value ) => new ViewAttributeElement( value ) );
+
+			modelRoot.appendChildren( new ModelText( 'foo', { italic: 'em' } ) );
+
+			// Set collapsed selection after "f".
+			const position = new ModelPosition( modelRoot, [ 1 ] );
+			modelDoc.selection.setRanges( [ new ModelRange( position, position ) ] );
+			modelDoc.selection._updateAttributes();
+
+			// Convert stuff.
+			dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			// Check if view structure is ok.
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
+
+			// Check if view selection is collapsed after "f" letter.
+			let ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
+			expect( ranges[ 0 ].start.parent ).to.be.instanceof( ViewText ); // "foo".
+			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
+
+			// Change selection attribute, convert it.
+			modelDoc.selection.setAttribute( 'italic', 'i' );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			// Check if view structure has changed.
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><i></i><em>oo</em></div>' );
+
+			// Check if view selection is inside new <em> element.
+			ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
+			expect( ranges[ 0 ].start.parent.name ).to.equal( 'i' );
+			expect( ranges[ 0 ].start.offset ).to.equal( 0 );
+
+			// Some more tests checking how selection attributes changes are converted:
+			modelDoc.selection.removeAttribute( 'italic' );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><em>oo</em></div>' );
+			ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges[ 0 ].start.parent.name ).to.equal( 'div' );
+			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
+
+			modelDoc.selection.setAttribute( 'italic', 'em' );
+			dispatcher.convertSelection( modelDoc.selection );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );
+			ranges = Array.from( viewSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.isEqual( ranges[ 0 ].end ) ).to.be.true;
+			expect( ranges[ 0 ].start.parent ).to.be.instanceof( ViewText ); // "foo".
+			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
+		} );
 	} );
 
 	describe( 'model attribute to view attribute conversion', () => {

+ 472 - 0
packages/ckeditor5-engine/tests/treecontroller/model-selection-to-view-converters.js

@@ -0,0 +1,472 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treecontroller */
+
+'use strict';
+
+import ModelDocument from '/ckeditor5/engine/treemodel/document.js';
+import ModelElement from '/ckeditor5/engine/treemodel/element.js';
+import ModelRange from '/ckeditor5/engine/treemodel/range.js';
+import ModelPosition from '/ckeditor5/engine/treemodel/position.js';
+
+import ViewContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
+import ViewAttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
+import ViewWriter from  '/ckeditor5/engine/treeview/writer.js';
+import ViewSelection from  '/ckeditor5/engine/treeview/selection.js';
+
+import Mapper from '/ckeditor5/engine/treecontroller/mapper.js';
+import ModelConversionDispatcher from '/ckeditor5/engine/treecontroller/modelconversiondispatcher.js';
+import {
+	convertRangeSelection,
+	convertCollapsedSelection,
+	convertSelectionAttribute,
+	clearAttributes
+} from '/ckeditor5/engine/treecontroller/model-selection-to-view-converters.js';
+
+import {
+	insertElement,
+	insertText,
+	wrap
+} from '/ckeditor5/engine/treecontroller/model-to-view-converters.js';
+
+import { stringify as stringifyView } from '/tests/engine/_utils/view.js';
+import { setData as setModelData } from '/tests/engine/_utils/model.js';
+
+let dispatcher, modelDoc, modelRoot, modelSelection, mapper, viewRoot, writer, viewSelection;
+
+beforeEach( () => {
+	modelDoc = new ModelDocument();
+	modelRoot = modelDoc.createRoot( 'main' );
+	modelSelection = modelDoc.selection;
+
+	viewRoot = new ViewContainerElement( 'viewRoot' );
+
+	mapper = new Mapper();
+	mapper.bindElements( modelRoot, viewRoot );
+
+	writer = new ViewWriter();
+	viewSelection = new ViewSelection();
+
+	dispatcher = new ModelConversionDispatcher( { mapper, writer, viewSelection } );
+
+	dispatcher.on( 'insert:$text', insertText() );
+	dispatcher.on( 'addAttribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) );
+
+	// Default selection converters.
+	dispatcher.on( 'selection', clearAttributes() );
+	dispatcher.on( 'selection', convertRangeSelection() );
+	dispatcher.on( 'selection', convertCollapsedSelection() );
+} );
+
+describe( 'default converters', () => {
+	beforeEach( () => {
+		// Selection converters for selection attributes.
+		dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
+		dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
+	} );
+
+	describe( 'range selection', () => {
+		it( 'in same container', () => {
+			test(
+				[ 1, 4 ],
+				'foobar',
+				'f{oob}ar'
+			);
+		} );
+
+		it( 'in same container, over attribute', () => {
+			test(
+				[ 1, 5 ],
+				'fo<$text bold=true>ob</$text>ar',
+				'f{o<strong>ob</strong>a}r'
+			);
+		} );
+
+		it( 'in same container, next to attribute', () => {
+			test(
+				[ 1, 2 ],
+				'fo<$text bold=true>ob</$text>ar',
+				'f{o}<strong>ob</strong>ar'
+			);
+		} );
+
+		it( 'in same attribute', () => {
+			test(
+				[ 2, 4 ],
+				'f<$text bold=true>ooba</$text>r',
+				'f<strong>o{ob}a</strong>r'
+			);
+		} );
+
+		it( 'in same attribute, selection same as attribute', () => {
+			test(
+				[ 2, 4 ],
+				'fo<$text bold=true>ob</$text>ar',
+				'fo{<strong>ob</strong>}ar'
+			);
+		} );
+
+		it( 'starts in text node, ends in attribute #1', () => {
+			test(
+				[ 1, 3 ],
+				'fo<$text bold=true>ob</$text>ar',
+				'f{o<strong>o}b</strong>ar'
+			);
+		} );
+
+		it( 'starts in text node, ends in attribute #2', () => {
+			test(
+				[ 1, 4 ],
+				'fo<$text bold=true>ob</$text>ar',
+				'f{o<strong>ob</strong>}ar'
+			);
+		} );
+
+		it( 'starts in attribute, ends in text node', () => {
+			test(
+				[ 3, 5 ],
+				'fo<$text bold=true>ob</$text>ar',
+				'fo<strong>o{b</strong>a}r'
+			);
+		} );
+
+		it( 'consumes consumable values properly', () => {
+			// Add callback that will fire before default ones.
+			// This should prevent default callback doing anything.
+			dispatcher.on( 'selection', ( evt, data, consumable ) => {
+				expect( consumable.consume( data.selection, 'selection' ) ).to.be.true;
+			}, null, 0 );
+
+			// Similar test case as the first in this suite.
+			test(
+				[ 1, 4 ],
+				'foobar',
+				'foobar' // No selection in view.
+			);
+		} );
+
+		it( 'should convert backward selection', () => {
+			test(
+				[ 1, 3, 'backward' ],
+				'foobar',
+				'f{oo}bar'
+			);
+
+			expect( viewSelection.focus.offset ).to.equal( 1 );
+		} );
+	} );
+
+	describe( 'collapsed selection', () => {
+		it( 'in container', () => {
+			test(
+				[ 1, 1 ],
+				'foobar',
+				'f{}oobar'
+			);
+		} );
+
+		it( 'in attribute', () => {
+			test(
+				[ 3, 3 ],
+				'f<$text bold=true>ooba</$text>r',
+				'f<strong>oo{}ba</strong>r'
+			);
+		} );
+
+		it( 'in container with extra attributes', () => {
+			test(
+				[ 1, 1 ],
+				'foobar',
+				'f<em>[]</em>oobar',
+				{ italic: true }
+			);
+		} );
+
+		it( 'in attribute with extra attributes', () => {
+			test(
+				[ 3, 3 ],
+				'f<$text bold=true>ooba</$text>r',
+				'f<strong>oo</strong><em><strong>[]</strong></em><strong>ba</strong>r',
+				{ italic: true }
+			);
+		} );
+
+		it( 'consumes consumable values properly', () => {
+			// Add callbacks that will fire before default ones.
+			// This should prevent default callbacks doing anything.
+			dispatcher.on( 'selection', ( evt, data, consumable ) => {
+				expect( consumable.consume( data.selection, 'selection' ) ).to.be.true;
+			}, null, 0 );
+
+			dispatcher.on( 'selectionAttribute:bold', ( evt, data, consumable ) => {
+				expect( consumable.consume( data.selection, 'selectionAttribute:bold' ) ).to.be.true;
+			}, null, 0 );
+
+			// Similar test case as above
+			test(
+				[ 3, 3 ],
+				'f<$text bold=true>ooba</$text>r',
+				'f<strong>ooba</strong>r' // No selection in view.
+			);
+		} );
+	} );
+} );
+
+describe( 'clean-up', () => {
+	describe( 'convertRangeSelection', () => {
+		it( 'should remove all ranges before adding new range', () => {
+			test(
+				[ 0, 2 ],
+				'foobar',
+				'{fo}obar'
+			);
+
+			test(
+				[ 3, 5 ],
+				'foobar',
+				'foo{ba}r'
+			);
+
+			expect( viewSelection.rangeCount ).to.equal( 1 );
+		} );
+	} );
+
+	describe( 'convertCollapsedSelection', () => {
+		it( 'should remove all ranges before adding new range', () => {
+			test(
+				[ 2, 2 ],
+				'foobar',
+				'fo{}obar'
+			);
+
+			test(
+				[ 3, 3 ],
+				'foobar',
+				'foo{}bar'
+			);
+
+			expect( viewSelection.rangeCount ).to.equal( 1 );
+		} );
+	} );
+
+	describe( 'clearAttributes', () => {
+		it( 'should remove all ranges before adding new range', () => {
+			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'b' ) ) );
+			dispatcher.on( 'addAttribute:style', wrap( new ViewAttributeElement( 'b' ) ) );
+
+			test(
+				[ 3, 3 ],
+				'foobar',
+				'foo<b>[]</b>bar',
+				{ bold: 'true' }
+			);
+
+			const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
+			modelDoc.selection.setRanges( [ modelRange ] );
+
+			dispatcher.convertSelection( modelDoc.selection );
+
+			expect( viewSelection.rangeCount ).to.equal( 1 );
+
+			const viewString = stringifyView( viewRoot, viewSelection, { showType: false } );
+			expect( viewString ).to.equal( '<viewRoot>f{}oobar</viewRoot>' );
+		} );
+	} );
+} );
+
+describe( 'using element creator for attributes conversion', () => {
+	beforeEach( () => {
+		function styleElementCreator( styleValue ) {
+			if ( styleValue == 'important' ) {
+				return new ViewAttributeElement( 'strong', { style: 'text-transform:uppercase;' } );
+			} else if ( styleValue == 'gold' ) {
+				return new ViewAttributeElement( 'span', { style: 'color:yellow;' } );
+			}
+		}
+
+		dispatcher.on( 'selectionAttribute:style', convertSelectionAttribute( styleElementCreator ) );
+		dispatcher.on( 'addAttribute:style', wrap( styleElementCreator ) );
+
+		dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
+	} );
+
+	describe( 'range selection', () => {
+		it( 'in same container, over attribute', () => {
+			test(
+				[ 1, 5 ],
+				'fo<$text style="gold">ob</$text>ar',
+				'f{o<span style="color:yellow;">ob</span>a}r'
+			);
+		} );
+
+		it( 'in same attribute', () => {
+			test(
+				[ 2, 4 ],
+				'f<$text style="gold">ooba</$text>r',
+				'f<span style="color:yellow;">o{ob}a</span>r'
+			);
+		} );
+
+		it( 'in same attribute, selection same as attribute', () => {
+			test(
+				[ 2, 4 ],
+				'fo<$text style="important">ob</$text>ar',
+				'fo{<strong style="text-transform:uppercase;">ob</strong>}ar'
+			);
+		} );
+
+		it( 'starts in attribute, ends in text node', () => {
+			test(
+				[ 3, 5 ],
+				'fo<$text style="important">ob</$text>ar',
+				'fo<strong style="text-transform:uppercase;">o{b</strong>a}r'
+			);
+		} );
+	} );
+
+	describe( 'collapsed selection', () => {
+		it( 'in attribute', () => {
+			test(
+				[ 3, 3 ],
+				'f<$text style="gold">ooba</$text>r',
+				'f<span style="color:yellow;">oo{}ba</span>r'
+			);
+		} );
+
+		it( 'in container with style attribute', () => {
+			test(
+				[ 1, 1 ],
+				'foobar',
+				'f<strong style="text-transform:uppercase;">[]</strong>oobar',
+				{ style: 'important' }
+			);
+		} );
+
+		it( 'in style attribute with extra attributes #1', () => {
+			test(
+				[ 3, 3 ],
+				'f<$text style="gold">ooba</$text>r',
+				'f<span style="color:yellow;">oo</span>' +
+				'<em><span style="color:yellow;">[]</span></em>' +
+				'<span style="color:yellow;">ba</span>r',
+				{ italic: true }
+			);
+		} );
+
+		it( 'in style attribute with extra attributes #2', () => {
+			// In contrary to test above, we don't have strong + span on the selection.
+			// This is because strong and span are both created by the same attribute.
+			// Since style="important" overwrites style="gold" on selection, we have only strong element.
+			// In example above, selection has both style and italic attribute.
+			test(
+				[ 3, 3 ],
+				'f<$text style="gold">ooba</$text>r',
+				'f<span style="color:yellow;">oo</span>' +
+				'<strong style="text-transform:uppercase;">[]</strong>' +
+				'<span style="color:yellow;">ba</span>r',
+				{ style: 'important' }
+			);
+		} );
+	} );
+} );
+
+describe( 'table cell selection converter', () => {
+	beforeEach( () => {
+		// "Universal" converter to convert table structure.
+		const tableConverter = insertElement( ( data ) => new ViewContainerElement( data.item.name ) );
+		dispatcher.on( 'insert:table', tableConverter );
+		dispatcher.on( 'insert:tr', tableConverter );
+		dispatcher.on( 'insert:td', tableConverter );
+
+		// Special converter for table cells.
+		dispatcher.on( 'selection', ( evt, data, consumable, conversionApi ) => {
+			const selection = data.selection;
+
+			if ( !consumable.test( selection, 'selection' ) || selection.isCollapsed ) {
+				return;
+			}
+
+			for ( let range of selection.getRanges() ) {
+				const node = range.start.nodeAfter;
+
+				if ( node == range.end.nodeBefore && node instanceof ModelElement && node.name == 'td' ) {
+					consumable.consume( selection, 'selection' );
+
+					let viewNode = conversionApi.mapper.toViewElement( node );
+					viewNode.addClass( 'selected' );
+				}
+			}
+		}, null, 0 );
+	} );
+
+	it( 'should not be used to convert selection that is not on table cell', () => {
+		test(
+			[ 1, 5 ],
+			'f<selection>o<$text bold=true>ob</$text>a</selection>r',
+			'f{o<strong>ob</strong>a}r'
+		);
+	} );
+
+	it( 'should add a class to the selected table cell', () => {
+		test(
+			// table tr#0, table tr#1
+			[ [ 0, 0, 0 ], [ 0, 0, 1 ] ],
+			'<table><tr><td>foo</td></tr><tr><td>bar</td></tr></table>',
+			'<table><tr><td class="selected">foo</td></tr><tr><td>bar</td></tr></table>'
+		);
+	} );
+
+	it( 'should not be used if selection contains more than just a table cell', () => {
+		test(
+			// table tr td#1, table tr#2
+			[ [ 0, 0, 0, 1 ], [ 0, 0, 2 ] ],
+			'<table><tr><td>foo</td><td>bar</td></tr></table>',
+			'<table><tr><td>f{oo</td><td>bar</td>]</tr></table>'
+		);
+	} );
+} );
+
+// Tests if the selection got correctly converted.
+// Because `setData` might use selection converters itself to set the selection, we can't use it
+// to set the selection (because then we would test converters using converters).
+// Instead, the `test` function expects to be passed `selectionPaths` which is an array containing two numbers or two arrays,
+// that are offsets or paths of selection positions in root element.
+function test( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) {
+	// Parse passed `modelInput` string and set it as current model.
+	setModelData( modelDoc, modelInput );
+
+	// Manually set selection ranges using passed `selectionPaths`.
+	const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ];
+	const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ];
+	const startPos = new ModelPosition( modelRoot, startPath );
+	const endPos = new ModelPosition( modelRoot, endPath );
+	const isBackward = selectionPaths[ 2 ] === 'backward';
+	modelSelection.setRanges( [ new ModelRange( startPos, endPos ) ], isBackward );
+
+	// Updated selection attributes according to model.
+	modelSelection._updateAttributes();
+
+	// And add or remove passed attributes.
+	for ( let key in selectionAttributes ) {
+		let value = selectionAttributes[ key ];
+
+		if ( value ) {
+			modelSelection.setAttribute( key, value );
+		} else {
+			modelSelection.removeAttribute( key );
+		}
+	}
+
+	// Remove view children since we do not want to convert deletion.
+	viewRoot.removeChildren( 0, viewRoot.getChildCount() );
+
+	// Convert model to view.
+	dispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
+	dispatcher.convertSelection( modelSelection );
+
+	// Stringify view and check if it is same as expected.
+	expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<viewRoot>' + expectedView + '</viewRoot>' );
+}

+ 72 - 0
packages/ckeditor5-engine/tests/treecontroller/modelconversiondispatcher.js

@@ -353,4 +353,76 @@ describe( 'ModelConversionDispatcher', () => {
 			expect( dispatcher.fire.calledWith( 'removeAttribute:attr:inside' ) ).to.be.false;
 		} );
 	} );
+
+	describe( 'convertSelection', () => {
+		beforeEach( () => {
+			dispatcher.off( 'selection' );
+
+			root.appendChildren( 'foobar' );
+			doc.selection.setRanges( [
+				new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 3 ] ) ),
+				new ModelRange( new ModelPosition( root, [ 4 ] ), new ModelPosition( root, [ 5 ] ) )
+			] );
+		} );
+
+		it( 'should fire selection event', () => {
+			sinon.spy( dispatcher, 'fire' );
+
+			dispatcher.convertSelection( doc.selection );
+
+			expect( dispatcher.fire.calledWith(
+				'selection',
+				{ selection: sinon.match.instanceOf( doc.selection.constructor ) }
+			) ).to.be.true;
+		} );
+
+		it( 'should prepare correct list of consumable values', () => {
+			doc.enqueueChanges( () => {
+				doc.batch()
+					.setAttr( 'bold', true, ModelRange.createFromElement( root ) )
+					.setAttr( '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, 'selectionAttribute:bold' ) ).to.be.true;
+				expect( consumable.test( data.selection, 'selectionAttribute:italic' ) ).to.be.null;
+			} );
+
+			dispatcher.convertSelection( doc.selection );
+		} );
+
+		it( 'should fire attributes events for selection', () => {
+			sinon.spy( dispatcher, 'fire' );
+
+			doc.enqueueChanges( () => {
+				doc.batch()
+					.setAttr( 'bold', true, ModelRange.createFromElement( root ) )
+					.setAttr( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+			} );
+
+			dispatcher.convertSelection( doc.selection );
+
+			expect( dispatcher.fire.calledWith( 'selectionAttribute:bold' ) ).to.be.true;
+			expect( dispatcher.fire.calledWith( 'selectionAttribute:italic' ) ).to.be.false;
+		} );
+
+		it( 'should not fire attributes events if attribute has been consumed', () => {
+			sinon.spy( dispatcher, 'fire' );
+
+			dispatcher.on( 'selection', ( evt, data, consumable ) => {
+				consumable.consume( data.selection, 'selectionAttribute:bold' );
+			} );
+
+			doc.enqueueChanges( () => {
+				doc.batch()
+					.setAttr( 'bold', true, ModelRange.createFromElement( root ) )
+					.setAttr( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+			} );
+
+			dispatcher.convertSelection( doc.selection );
+
+			expect( dispatcher.fire.calledWith( 'selectionAttribute:bold' ) ).to.be.false;
+		} );
+	} );
 } );

+ 6 - 6
packages/ckeditor5-engine/tests/treemodel/composer/composer.js

@@ -23,18 +23,18 @@ describe( 'Composer', () => {
 
 	describe( 'constructor', () => {
 		it( 'attaches deleteContents default listener', () => {
-			setData( document, 'main', '<p>f<selection>oo</p><p>ba</selection>r</p>' );
+			setData( document, '<p>f<selection>oo</p><p>ba</selection>r</p>' );
 
 			const batch = document.batch();
 
 			composer.fire( 'deleteContents', { batch, selection: document.selection } );
 
-			expect( getData( document, 'main' ) ).to.equal( '<p>f</p><p>r</p>' );
+			expect( getData( document ) ).to.equal( '<p>f<selection /></p><p>r</p>' );
 			expect( batch.deltas ).to.not.be.empty;
 		} );
 
 		it( 'attaches deleteContents default listener which passes options', () => {
-			setData( document, 'main', '<p>f<selection>oo</p><p>ba</selection>r</p>' );
+			setData( document, '<p>f<selection>oo</p><p>ba</selection>r</p>' );
 
 			const batch = document.batch();
 
@@ -44,11 +44,11 @@ describe( 'Composer', () => {
 				options: { merge: true }
 			} );
 
-			expect( getData( document, 'main' ) ).to.equal( '<p>fr</p>' );
+			expect( getData( document ) ).to.equal( '<p>f<selection />r</p>' );
 		} );
 
 		it( 'attaches modifySelection default listener', () => {
-			setData( document, 'main', '<p>foo<selection />bar</p>' );
+			setData( document, '<p>foo<selection />bar</p>' );
 
 			composer.fire( 'modifySelection', {
 				selection: document.selection,
@@ -57,7 +57,7 @@ describe( 'Composer', () => {
 				}
 			} );
 
-			expect( getData( document, 'main', { selection: true } ) )
+			expect( getData( document ) )
 				.to.equal( '<p>fo<selection backward>o</selection>bar</p>' );
 		} );
 	} );

+ 2 - 2
packages/ckeditor5-engine/tests/treemodel/composer/deletecontents.js

@@ -215,11 +215,11 @@ describe( 'Delete utils', () => {
 
 		function test( title, input, output, options ) {
 			it( title, () => {
-				setData( document, 'main', input );
+				setData( document, input );
 
 				deleteContents( document.batch(), document.selection, options );
 
-				expect( getData( document, 'main', { selection: true } ) ).to.equal( output );
+				expect( getData( document ) ).to.equal( output );
 			} );
 		}
 	} );

+ 2 - 2
packages/ckeditor5-engine/tests/treemodel/composer/modifyselection.js

@@ -229,11 +229,11 @@ describe( 'Delete utils', () => {
 
 	function test( title, input, output, options ) {
 		it( title, () => {
-			setData( document, 'main', input );
+			setData( document, input );
 
 			modifySelection( document.selection, options );
 
-			expect( getData( document, 'main', { selection: true } ) ).to.equal( output );
+			expect( getData( document ) ).to.equal( output );
 		} );
 	}
 } );

+ 9 - 11
packages/ckeditor5-engine/tests/treemodel/delta/transform/_utils/utils.js

@@ -152,17 +152,15 @@ export function expectDelta( delta, expected ) {
 
 export function expectOperation( op, params ) {
 	for ( let i in params ) {
-		if ( params.hasOwnProperty( i ) ) {
-			if ( i == 'type' ) {
-				expect( op ).to.be.instanceof( params[ i ] );
-			}
-			else if ( i == 'nodes' ) {
-				expect( op.nodeList._nodes ).to.deep.equal( params[ i ] );
-			} else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
-				expect( op[ i ].isEqual( params[ i ] ) ).to.be.true;
-			} else {
-				expect( op[ i ] ).to.equal( params[ i ] );
-			}
+		if ( i == 'type' ) {
+			expect( op ).to.be.instanceof( params[ i ] );
+		}
+		else if ( i == 'nodes' ) {
+			expect( op.nodeList._nodes ).to.deep.equal( params[ i ] );
+		} else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
+			expect( op[ i ].isEqual( params[ i ] ) ).to.be.true;
+		} else {
+			expect( op[ i ] ).to.equal( params[ i ] );
 		}
 	}
 }

+ 19 - 0
packages/ckeditor5-engine/tests/treemodel/operation/moveoperation.js

@@ -239,6 +239,25 @@ describe( 'MoveOperation', () => {
 		expect( p.getChild( 0 ).character ).to.equal( 'b' );
 	} );
 
+	it( 'should not throw when operation paths looks like incorrect but move is between different roots', () => {
+		let p = new Element( 'p' );
+		root.insertChildren( 0, [ 'a', p, 'b' ] );
+		doc.graveyard.insertChildren( 0, [ 'abc' ] );
+
+		let operation = new MoveOperation(
+			new Position( doc.graveyard, [ 0 ] ),
+			2,
+			new Position( root, [ 1, 0 ] ),
+			doc.version
+		);
+
+		expect(
+			() => {
+				doc.applyOperation( operation );
+			}
+		).not.to.throw();
+	} );
+
 	it( 'should create MoveOperation with the same parameters when cloned', () => {
 		let sourcePosition = new Position( root, [ 0 ] );
 		let targetPosition = new Position( root, [ 1 ] );

+ 24 - 3
packages/ckeditor5-engine/tests/treemodel/selection.js

@@ -66,20 +66,41 @@ describe( 'Selection', () => {
 	} );
 
 	describe( 'isCollapsed', () => {
-		it( 'should be true if all ranges are collapsed', () => {
+		it( 'should return true for default range', () => {
+			expect( selection.isCollapsed ).to.be.true;
+		} );
+
+		it( 'should return true when there is single collapsed ranges', () => {
 			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
 
 			expect( selection.isCollapsed ).to.be.true;
 		} );
 
-		it( 'should be false when it has a range that is not collapsed', () => {
+		it( 'should return false when there are multiple ranges', () => {
+			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+			selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
+
+			expect( selection.isCollapsed ).to.be.false;
+		} );
+
+		it( 'should return false when there is not collapsed range', () => {
 			selection.addRange( range );
 
 			expect( selection.isCollapsed ).to.be.false;
+		} );
+	} );
+
+	describe( 'rangeCount', () => {
+		it( 'should return proper range count', () => {
+			expect( selection.rangeCount ).to.equal( 1 );
 
 			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
 
-			expect( selection.isCollapsed ).to.be.false;
+			expect( selection.rangeCount ).to.equal( 1 );
+
+			selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
+
+			expect( selection.rangeCount ).to.equal( 2 );
 		} );
 	} );
 

+ 47 - 3
packages/ckeditor5-engine/tests/treeview/attributeelement.js

@@ -3,13 +3,13 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
 import AttributeElement from '/ckeditor5/engine/treeview/attributeelement.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
-import { DEFAULT_PRIORITY } from '/ckeditor5/engine/treeview/attributeelement.js';
+import { parse } from '/tests/engine/_utils/view.js';
 
 describe( 'AttributeElement', () => {
 	describe( 'constructor', () => {
@@ -19,7 +19,7 @@ describe( 'AttributeElement', () => {
 			expect( el ).to.be.an.instanceof( AttributeElement );
 			expect( el ).to.be.an.instanceof( Element );
 			expect( el ).to.have.property( 'name' ).that.equals( 'strong' );
-			expect( el ).to.have.property( 'priority' ).that.equals( DEFAULT_PRIORITY );
+			expect( el ).to.have.property( 'priority' ).that.equals( AttributeElement.DEFAULT_PRIORITY );
 		} );
 	} );
 
@@ -56,4 +56,48 @@ describe( 'AttributeElement', () => {
 			expect( b1.isSimilar( b2 ) ).to.be.false;
 		} );
 	} );
+
+	describe( 'getFillerOffset', () => {
+		it( 'should return position 0 if it is the only element in the container', () => {
+			const { selection } = parse( '<container:p><attribute:b>[]</attribute:b></container:p>' );
+			const attribute = selection.getFirstPosition().parent;
+
+			expect( attribute.getFillerOffset() ).to.equals( 0 );
+		} );
+
+		it( 'should return position 0 if it is the only nested element in the container', () => {
+			const { selection } = parse(
+				'<container:p><attribute:b><attribute:i>[]</attribute:i></attribute:b></container:p>' );
+			const attribute = selection.getFirstPosition().parent;
+
+			expect( attribute.getFillerOffset() ).to.equals( 0 );
+		} );
+
+		it( 'should return null if element contains another element', () => {
+			const attribute = parse( '<attribute:b><attribute:i></attribute:i></attribute:b>' );
+
+			expect( attribute.getFillerOffset() ).to.be.null;
+		} );
+
+		it( 'should return null if element contains text', () => {
+			const attribute = parse( '<attribute:b>text</attribute:b>' );
+
+			expect( attribute.getFillerOffset() ).to.be.null;
+		} );
+
+		it( 'should return null if container element contains text', () => {
+			const { selection } = parse( '<container:p><attribute:b>[]</attribute:b>foo</container:p>' );
+			const attribute = selection.getFirstPosition().parent;
+
+			expect( attribute.getFillerOffset() ).to.be.null;
+		} );
+
+		it( 'should return null if it is the parent contains text', () => {
+			const { selection } = parse(
+				'<container:p><attribute:b><attribute:i>[]</attribute:i>foo</attribute:b></container:p>' );
+			const attribute = selection.getFirstPosition().parent;
+
+			expect( attribute.getFillerOffset() ).to.be.null;
+		} );
+	} );
 } );

+ 12 - 1
packages/ckeditor5-engine/tests/treeview/containerelement.js

@@ -3,12 +3,13 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
 import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
+import { parse } from '/tests/engine/_utils/view.js';
 
 describe( 'ContainerElement', () => {
 	describe( 'constructor', () => {
@@ -20,4 +21,14 @@ describe( 'ContainerElement', () => {
 			expect( el ).to.have.property( 'name' ).that.equals( 'p' );
 		} );
 	} );
+
+	describe( 'getFillerOffset', () => {
+		it( 'should return position 0 if element is empty', () => {
+			expect( parse( '<container:p></container:p>' ).getFillerOffset() ).to.equals( 0 );
+		} );
+
+		it( 'should return null if element is not empty', () => {
+			expect( parse( '<container:p>foo</container:p>' ).getFillerOffset() ).to.be.null;
+		} );
+	} );
 } );

+ 233 - 0
packages/ckeditor5-engine/tests/treeview/document/document.js

@@ -0,0 +1,233 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, browser-only */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/treeview/document.js';
+import Observer from '/ckeditor5/engine/treeview/observer/observer.js';
+import Renderer from '/ckeditor5/engine/treeview/renderer.js';
+import Writer from '/ckeditor5/engine/treeview/writer.js';
+import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+
+import count from '/ckeditor5/utils/count.js';
+
+describe( 'Document', () => {
+	let ObserverMock, ObserverMockGlobalCount, instantiated, enabled;
+
+	beforeEach( () => {
+		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++;
+			}
+		};
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create Document with all properties', () => {
+			const viewDocument = new Document();
+
+			expect( count( viewDocument.domRoots ) ).to.equal( 0 );
+			expect( count( viewDocument.viewRoots ) ).to.equal( 0 );
+			expect( count( viewDocument._observers ) ).to.equal( 0 );
+			expect( viewDocument ).to.have.property( 'renderer' ).that.is.instanceOf( Renderer );
+			expect( viewDocument ).to.have.property( 'writer' ).that.is.instanceOf( Writer );
+			expect( viewDocument ).to.have.property( 'domConverter' ).that.is.instanceOf( DomConverter );
+		} );
+	} );
+
+	describe( 'createRoot', () => {
+		it( 'should create root', () => {
+			const domP = document.createElement( 'p' );
+			const domDiv = document.createElement( 'div' );
+			domDiv.setAttribute( 'id', 'editor' );
+			domDiv.appendChild( domP );
+
+			const viewDocument = new Document();
+			const ret = viewDocument.createRoot( domDiv, 'editor' );
+
+			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
+			expect( count( viewDocument.viewRoots ) ).to.equal( 1 );
+
+			const domRoot = viewDocument.domRoots.get( 'editor' );
+			const viewRoot = viewDocument.viewRoots.get( 'editor' );
+
+			expect( ret ).to.equal( viewRoot );
+
+			expect( domRoot ).to.equal( domDiv );
+			expect( viewDocument.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domDiv );
+			expect( viewRoot.name ).to.equal( 'div' );
+			expect( viewRoot.getAttribute( 'id' ) ).to.equal( 'editor' );
+			expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
+		} );
+
+		it( 'should call observe on each observer', () => {
+			const 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 );
+
+			viewDocument.createRoot( document.createElement( 'div' ), 'root1' );
+
+			sinon.assert.calledOnce( observerMock.observe );
+			sinon.assert.calledOnce( observerMockGlobalCount.observe );
+		} );
+
+		it( 'should create "main" root by default', () => {
+			const domDiv = document.createElement( 'div' );
+
+			const viewDocument = new Document();
+			const ret = viewDocument.createRoot( domDiv );
+
+			expect( count( viewDocument.domRoots ) ).to.equal( 1 );
+			expect( count( viewDocument.viewRoots ) ).to.equal( 1 );
+
+			const domRoot = viewDocument.domRoots.get( 'main' );
+			const viewRoot = viewDocument.viewRoots.get( 'main' );
+
+			expect( ret ).to.equal( viewRoot );
+
+			expect( domRoot ).to.equal( domDiv );
+		} );
+	} );
+
+	describe( 'getRoot', () => {
+		it( 'should return "main" root', () => {
+			const viewDocument = new Document();
+			viewDocument.createRoot( document.createElement( 'div' ) );
+
+			expect( count( viewDocument.viewRoots ) ).to.equal( 1 );
+
+			expect( viewDocument.getRoot() ).to.equal( viewDocument.viewRoots.get( 'main' ) );
+		} );
+
+		it( 'should return named root', () => {
+			const viewDocument = new Document();
+			viewDocument.createRoot( document.createElement( 'h1' ), 'header' );
+
+			expect( count( viewDocument.viewRoots ) ).to.equal( 1 );
+
+			expect( viewDocument.getRoot( 'header' ) ).to.equal( viewDocument.viewRoots.get( 'header' ) );
+		} );
+	} );
+
+	describe( 'addObserver', () => {
+		let viewDocument;
+
+		beforeEach( () => {
+			viewDocument = new Document( document.createElement( 'div' ) );
+			viewDocument.renderer.render = sinon.spy();
+		} );
+
+		it( 'should be instantiated and enabled on adding', () => {
+			const observerMock = viewDocument.addObserver( ObserverMock );
+
+			expect( viewDocument._observers.size ).to.equal( 1 );
+
+			expect( observerMock ).to.have.property( 'document', viewDocument );
+			sinon.assert.calledOnce( observerMock.enable );
+		} );
+
+		it( 'should return observer instance ever 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( 1 );
+			expect( instantiated ).to.equal( 1 );
+			expect( enabled ).to.equal( 1 );
+
+			viewDocument.addObserver( ObserverMock );
+			expect( viewDocument._observers.size ).to.equal( 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( 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', () => {
+			viewDocument.createRoot( document.createElement( 'div' ), 'root1' );
+			viewDocument.createRoot( document.createElement( 'div' ), 'root2' );
+
+			const observerMock = viewDocument.addObserver( ObserverMock );
+
+			sinon.assert.calledTwice( observerMock.observe );
+		} );
+	} );
+
+	describe( 'getObserver', () => {
+		it( 'should return observer it it is added', () => {
+			const viewDocument = new Document();
+
+			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 viewDocument = new Document();
+			const getObserverMock = viewDocument.getObserver( ObserverMock );
+
+			expect( getObserverMock ).to.be.undefined();
+		} );
+	} );
+} );

+ 12 - 12
packages/ckeditor5-engine/tests/treeview/integration.js → packages/ckeditor5-engine/tests/treeview/document/integration.js

@@ -3,36 +3,36 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
-import TreeElement from '/ckeditor5/engine/treeview/element.js';
+import Document from '/ckeditor5/engine/treeview/document.js';
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
 
-describe( 'TreeView integration', () => {
+describe( 'Document integration', () => {
 	it( 'should remove content of the DOM', () => {
 		const domP = document.createElement( 'p' );
 		const domDiv = document.createElement( 'div' );
 		domDiv.setAttribute( 'id', 'editor' );
 		domDiv.appendChild( domP );
 
-		const treeView = new TreeView();
-		treeView.createRoot( domDiv, 'editor' );
-		treeView.render();
+		const viewDocument = new Document();
+		viewDocument.createRoot( domDiv, 'editor' );
+		viewDocument.render();
 
 		expect( domDiv.childNodes.length ).to.equal( 0 );
 		expect( domDiv.getAttribute( 'id' ) ).to.equal( 'editor' );
 	} );
 
-	it( 'should render changes in the TreeView', () => {
+	it( 'should render changes in the Document', () => {
 		const domDiv = document.createElement( 'div' );
 
-		const treeView = new TreeView();
-		treeView.createRoot( domDiv, 'editor' );
+		const viewDocument = new Document();
+		viewDocument.createRoot( domDiv, 'editor' );
 
-		treeView.viewRoots.get( 'editor' ).appendChildren( new TreeElement( 'p' ) );
-		treeView.render();
+		viewDocument.viewRoots.get( 'editor' ).appendChildren( new ViewElement( 'p' ) );
+		viewDocument.render();
 
 		expect( domDiv.childNodes.length ).to.equal( 1 );
 		expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );

+ 0 - 0
packages/ckeditor5-engine/tests/treeview/manual/mutationobserver.html → packages/ckeditor5-engine/tests/treeview/document/jumpoverinlinefiller.html


+ 101 - 0
packages/ckeditor5-engine/tests/treeview/document/jumpoverinlinefiller.js

@@ -0,0 +1,101 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, browser-only */
+
+'use strict';
+
+import ViewRange from '/ckeditor5/engine/treeview/range.js';
+import ViewDocument from '/ckeditor5/engine/treeview/document.js';
+import KeyObserver from '/ckeditor5/engine/treeview/observer/keyobserver.js';
+import { INLINE_FILLER_LENGTH, isInlineFiller, startsWithFiller } from '/ckeditor5/engine/treeview/filler.js';
+
+import { keyCodes } from '/ckeditor5/utils/keyboard.js';
+
+import { parse, setData } from '/tests/engine/_utils/view.js';
+
+describe( 'Document', () => {
+	let viewDocument;
+
+	beforeEach( () => {
+		viewDocument = new ViewDocument();
+
+		viewDocument.addObserver( KeyObserver );
+		viewDocument.createRoot( document.getElementById( 'editor' ) );
+
+		document.getSelection().removeAllRanges();
+	} );
+
+	describe( 'jump over inline filler hack', () => {
+		it( 'should jump over inline filler when left arrow is pressed after inline filler', () => {
+			setData( viewDocument, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			viewDocument.render();
+
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
+
+			const domRange = document.getSelection().getRangeAt( 0 );
+			expect( isInlineFiller( domRange.startContainer ) ).to.be.true;
+			expect( domRange.startOffset ).to.equal( 0 );
+			expect( domRange.collapsed ).to.be.true;
+		} );
+
+		it( 'should do nothing when another key is pressed', () => {
+			setData( viewDocument, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			viewDocument.render();
+
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) } );
+
+			const domRange = document.getSelection().getRangeAt( 0 );
+			expect( isInlineFiller( domRange.startContainer ) ).to.be.true;
+			expect( domRange.startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domRange.collapsed ).to.be.true;
+		} );
+
+		it( 'should do nothing if range is not collapsed', () => {
+			setData( viewDocument, '<container:p>foo<attribute:b>{x}</attribute:b>bar</container:p>' );
+			viewDocument.render();
+
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
+
+			const domRange = document.getSelection().getRangeAt( 0 );
+			expect( domRange.startContainer.data ).to.equal( 'x' );
+			expect( domRange.startOffset ).to.equal( 0 );
+			expect( domRange.endContainer.data ).to.equal( 'x' );
+			expect( domRange.endOffset ).to.equal( 1 );
+		} );
+
+		it( 'should do nothing if node does not start with filler', () => {
+			setData( viewDocument, '<container:p>foo<attribute:b>{}x</attribute:b>bar</container:p>' );
+			viewDocument.render();
+
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
+
+			const domRange = document.getSelection().getRangeAt( 0 );
+			expect( domRange.startContainer.data ).to.equal( 'x' );
+			expect( domRange.startOffset ).to.equal( 0 );
+			expect( domRange.collapsed ).to.be.true;
+		} );
+
+		it( 'should do nothing if caret is not directly before filler', () => {
+			setData( viewDocument, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			viewDocument.render();
+
+			// '<container:p>foo<attribute:b>x{}</attribute:b>bar</container:p>'
+			const viewB = viewDocument.selection.getFirstPosition().parent;
+			const viewTextX = parse( 'x' );
+			viewB.appendChildren( viewTextX );
+			viewDocument.selection.removeAllRanges();
+			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) );
+			viewDocument.render();
+
+			viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
+
+			const domRange = document.getSelection().getRangeAt( 0 );
+			expect( startsWithFiller( domRange.startContainer ) ).to.be.true;
+			expect( domRange.startOffset ).to.equal( INLINE_FILLER_LENGTH + 1 );
+			expect( domRange.collapsed ).to.be.true;
+		} );
+	} );
+} );

+ 0 - 586
packages/ckeditor5-engine/tests/treeview/domconverter.js

@@ -1,586 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* bender-tags: treeview */
-
-'use strict';
-
-import count from '/ckeditor5/utils/count.js';
-import ViewElement from '/ckeditor5/engine/treeview/element.js';
-import ViewText from '/ckeditor5/engine/treeview/text.js';
-import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
-import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
-
-describe( 'DomConverter', () => {
-	let converter;
-
-	before( () => {
-		converter = new DomConverter();
-	} );
-
-	describe( 'bindElements', () => {
-		it( 'should bind elements', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.getCorrespondingView( domElement ) ).to.equal( viewElement );
-			expect( converter.getCorrespondingDom( viewElement ) ).to.equal( domElement );
-		} );
-	} );
-
-	describe( 'bindDocumentFragments', () => {
-		it( 'should bind document fragments', () => {
-			const domFragment = document.createDocumentFragment();
-			const viewFragment = new ViewDocumentFragment();
-
-			converter.bindDocumentFragments( domFragment, viewFragment );
-
-			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
-			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
-		} );
-	} );
-
-	describe( 'compareNodes', () => {
-		it( 'should return false for nodes not matching types', () => {
-			const domElement = document.createElement( 'p' );
-			const viewText = new ViewText( 'foo' );
-
-			expect( converter.compareNodes( domElement, viewText ) ).to.be.false;
-		} );
-
-		it( 'should return true for bound elements', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.compareNodes( domElement, viewElement ) ).to.be.true;
-		} );
-
-		it( 'should return true for the same texts', () => {
-			const domText = document.createTextNode( 'foo' );
-			const viewText = new ViewText( 'foo' );
-
-			expect( converter.compareNodes( domText, viewText ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'domToView', () => {
-		it( 'should create tree of view elements from DOM elements', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.setAttribute( 'class', 'foo' );
-
-			domP.appendChild( domImg );
-			domP.appendChild( domText );
-
-			const viewImg = new ViewElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const viewP = converter.domToView( domP );
-
-			expect( viewP ).to.be.an.instanceof( ViewElement );
-			expect( viewP.name ).to.equal( 'p' );
-
-			expect( viewP.getAttribute( 'class' ) ).to.equal( 'foo' );
-			expect( count( viewP.getAttributeKeys() ) ).to.equal( 1 );
-
-			expect( viewP.getChildCount() ).to.equal( 2 );
-			expect( viewP.getChild( 0 ).name ).to.equal( 'img' );
-			expect( viewP.getChild( 1 ).data ).to.equal( 'foo' );
-
-			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
-			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domImg );
-		} );
-
-		it( 'should create tree of view elements from DOM elements and bind elements', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.setAttribute( 'class', 'foo' );
-
-			domP.appendChild( domImg );
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP, { bind: true } );
-
-			expect( viewP ).to.be.an.instanceof( ViewElement );
-			expect( viewP.name ).to.equal( 'p' );
-
-			expect( viewP.getAttribute( 'class' ) ).to.equal( 'foo' );
-			expect( count( viewP.getAttributeKeys() ) ).to.equal( 1 );
-
-			expect( viewP.getChildCount() ).to.equal( 2 );
-			expect( viewP.getChild( 0 ).name ).to.equal( 'img' );
-			expect( viewP.getChild( 1 ).data ).to.equal( 'foo' );
-
-			expect( converter.getCorrespondingDom( viewP ) ).to.equal( domP );
-			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
-		} );
-
-		it( 'should create tree of view elements from DOM element without children', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.setAttribute( 'class', 'foo' );
-
-			domP.appendChild( domImg );
-			domP.appendChild( domText );
-
-			const viewImg = new ViewElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const viewP = converter.domToView( domP, { withChildren: false } );
-
-			expect( viewP ).to.be.an.instanceof( ViewElement );
-			expect( viewP.name ).to.equal( 'p' );
-
-			expect( viewP.getAttribute( 'class' ) ).to.equal( 'foo' );
-			expect( count( viewP.getAttributeKeys() ) ).to.equal( 1 );
-
-			expect( viewP.getChildCount() ).to.equal( 0 );
-			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
-		} );
-
-		it( 'should create view document fragment from DOM document fragment', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domFragment = document.createDocumentFragment();
-
-			domFragment.appendChild( domImg );
-			domFragment.appendChild( domText );
-
-			const viewFragment = converter.domToView( domFragment, { bind: true } );
-
-			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
-			expect( viewFragment.getChildCount() ).to.equal( 2 );
-			expect( viewFragment.getChild( 0 ).name ).to.equal( 'img' );
-			expect( viewFragment.getChild( 1 ).data ).to.equal( 'foo' );
-
-			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
-			expect( converter.getCorrespondingDom( viewFragment.getChild( 0 ) ) ).to.equal( domFragment.childNodes[ 0 ] );
-		} );
-
-		it( 'should create view document fragment from DOM document fragment without children', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domFragment = document.createDocumentFragment();
-
-			domFragment.appendChild( domImg );
-			domFragment.appendChild( domText );
-
-			const viewImg = new ViewElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const viewFragment = converter.domToView( domFragment, { withChildren: false } );
-
-			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
-
-			expect( viewFragment.getChildCount() ).to.equal( 0 );
-			expect( converter.getCorrespondingDom( viewFragment ) ).to.not.equal( domFragment );
-		} );
-
-		it( 'should return already bind document fragment', () => {
-			const domFragment = document.createDocumentFragment();
-			const viewFragment = new ViewDocumentFragment();
-
-			converter.bindDocumentFragments( domFragment, viewFragment );
-
-			const viewFragment2 = converter.domToView( domFragment );
-
-			expect( viewFragment2 ).to.equal( viewFragment );
-		} );
-	} );
-
-	describe( 'viewToDom', () => {
-		it( 'should create tree of DOM elements from view elements', () => {
-			const viewImg = new ViewElement( 'img' );
-			const viewText = new ViewText( 'foo' );
-			const viewP = new ViewElement( 'p' );
-
-			viewP.setAttribute( 'class', 'foo' );
-
-			viewP.appendChildren( viewImg );
-			viewP.appendChildren( viewText );
-
-			const domImg = document.createElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const domP = converter.viewToDom( viewP, document );
-
-			expect( domP ).to.be.an.instanceof( HTMLElement );
-			expect( domP.tagName ).to.equal( 'P' );
-
-			expect( domP.getAttribute( 'class' ) ).to.equal( 'foo' );
-			expect( domP.attributes.length ).to.equal( 1 );
-
-			expect( domP.childNodes.length ).to.equal( 2 );
-			expect( domP.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
-			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
-
-			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
-			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewImg );
-		} );
-
-		it( 'should create tree of DOM elements from view elements and bind elements', () => {
-			const viewImg = new ViewElement( 'img' );
-			const viewText = new ViewText( 'foo' );
-			const viewP = new ViewElement( 'p' );
-
-			viewP.setAttribute( 'class', 'foo' );
-
-			viewP.appendChildren( viewImg );
-			viewP.appendChildren( viewText );
-
-			const domP = converter.viewToDom( viewP, document, { bind: true } );
-
-			expect( domP ).to.be.an.instanceof( HTMLElement );
-			expect( domP.tagName ).to.equal( 'P' );
-
-			expect( domP.getAttribute( 'class' ) ).to.equal( 'foo' );
-			expect( domP.attributes.length ).to.equal( 1 );
-
-			expect( domP.childNodes.length ).to.equal( 2 );
-			expect( domP.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
-			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
-
-			expect( converter.getCorrespondingView( domP ) ).to.equal( viewP );
-			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
-		} );
-
-		it( 'should create tree of DOM elements from view element without children', () => {
-			const viewImg = new ViewElement( 'img' );
-			const viewText = new ViewText( 'foo' );
-			const viewP = new ViewElement( 'p' );
-
-			viewP.setAttribute( 'class', 'foo' );
-
-			viewP.appendChildren( viewImg );
-			viewP.appendChildren( viewText );
-
-			const domImg = document.createElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const domP = converter.viewToDom( viewP, document, { withChildren: false } );
-
-			expect( domP ).to.be.an.instanceof( HTMLElement );
-			expect( domP.tagName ).to.equal( 'P' );
-
-			expect( domP.getAttribute( 'class' ) ).to.equal( 'foo' );
-			expect( domP.attributes.length ).to.equal( 1 );
-
-			expect( domP.childNodes.length ).to.equal( 0 );
-			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
-		} );
-
-		it( 'should create DOM document fragment from view document fragment and bind elements', () => {
-			const viewImg = new ViewElement( 'img' );
-			const viewText = new ViewText( 'foo' );
-			const viewFragment = new ViewDocumentFragment();
-
-			viewFragment.appendChildren( viewImg );
-			viewFragment.appendChildren( viewText );
-
-			const domFragment = converter.viewToDom( viewFragment, document, { bind: true } );
-
-			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
-			expect( domFragment.childNodes.length ).to.equal( 2 );
-			expect( domFragment.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
-			expect( domFragment.childNodes[ 1 ].data ).to.equal( 'foo' );
-
-			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
-			expect( converter.getCorrespondingView( domFragment.childNodes[ 0 ] ) ).to.equal( viewFragment.getChild( 0 ) );
-		} );
-
-		it( 'should create DOM document fragment from view document without children', () => {
-			const viewImg = new ViewElement( 'img' );
-			const viewText = new ViewText( 'foo' );
-			const viewFragment = new ViewDocumentFragment();
-
-			viewFragment.appendChildren( viewImg );
-			viewFragment.appendChildren( viewText );
-
-			const domImg = document.createElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const domFragment = converter.viewToDom( viewFragment, document, { withChildren: false } );
-
-			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
-
-			expect( domFragment.childNodes.length ).to.equal( 0 );
-			expect( converter.getCorrespondingView( domFragment ) ).not.to.equal( viewFragment );
-		} );
-
-		it( 'should return already bind document fragment', () => {
-			const domFragment = document.createDocumentFragment();
-			const viewFragment = new ViewDocumentFragment();
-
-			converter.bindDocumentFragments( domFragment, viewFragment );
-
-			const domFragment2 = converter.viewToDom( viewFragment );
-
-			expect( domFragment2 ).to.equal( domFragment );
-		} );
-	} );
-
-	describe( 'getCorrespondingView', () => {
-		it( 'should return corresponding view element if element is passed', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.getCorrespondingView( domElement ) ).to.equal( viewElement );
-		} );
-
-		it( 'should return corresponding view text if text is passed', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 0 );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingView( domText ) ).to.equal( viewText );
-		} );
-
-		it( 'should return corresponding view document fragment', () => {
-			const domFragment = document.createDocumentFragment();
-			const viewFragment = converter.domToView( domFragment );
-
-			converter.bindElements( domFragment, viewFragment );
-
-			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
-		} );
-	} );
-
-	describe( 'getCorrespondingViewElement', () => {
-		it( 'should return corresponding view element', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.getCorrespondingViewElement( domElement ) ).to.equal( viewElement );
-		} );
-	} );
-
-	describe( 'getCorrespondingViewDocumentFragment', () => {
-		it( 'should return corresponding view document fragment', () => {
-			const domFragment = document.createDocumentFragment();
-			const viewFragment = converter.domToView( domFragment );
-
-			converter.bindElements( domFragment, viewFragment );
-
-			expect( converter.getCorrespondingViewDocumentFragment( domFragment ) ).to.equal( viewFragment );
-		} );
-	} );
-
-	describe( 'getCorrespondingViewText', () => {
-		it( 'should return corresponding view text based on sibling', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domImg );
-			domP.appendChild( domText );
-
-			const viewImg = new ViewElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 1 );
-
-			expect( converter.getCorrespondingViewText( domText ) ).to.equal( viewText );
-		} );
-
-		it( 'should return corresponding view text based on parent', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 0 );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingViewText( domText ) ).to.equal( viewText );
-		} );
-
-		it( 'should return null if sibling is not bound', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domImg );
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
-		} );
-
-		it( 'should return null if sibling is not element', () => {
-			const domTextFoo = document.createTextNode( 'foo' );
-			const domTextBar = document.createTextNode( 'bar' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domTextFoo );
-			domP.appendChild( domTextBar );
-
-			const viewP = converter.domToView( domP );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingViewText( domTextBar ) ).to.be.null;
-		} );
-
-		it( 'should return null if parent is not bound', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
-		} );
-	} );
-
-	describe( 'getCorrespondingDom', () => {
-		it( 'should return corresponding DOM element if element was passed', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.getCorrespondingDom( viewElement ) ).to.equal( domElement );
-		} );
-
-		it( 'should return corresponding DOM text if text was passed', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 0 );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingDom( viewText ) ).to.equal( domText );
-		} );
-
-		it( 'should return corresponding DOM document fragment', () => {
-			const domFragment = document.createDocumentFragment();
-			const viewFragment = new ViewDocumentFragment();
-
-			converter.bindElements( domFragment, viewFragment );
-
-			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
-		} );
-	} );
-
-	describe( 'getCorrespondingDomElement', () => {
-		it( 'should return corresponding DOM element', () => {
-			const domElement = document.createElement( 'p' );
-			const viewElement = new ViewElement( 'p' );
-
-			converter.bindElements( domElement, viewElement );
-
-			expect( converter.getCorrespondingDomElement( viewElement ) ).to.equal( domElement );
-		} );
-	} );
-
-	describe( 'getCorrespondingDomDocumentFragment', () => {
-		it( 'should return corresponding DOM document fragment', () => {
-			const domFragment = document.createDocumentFragment();
-			const viewFragment = new ViewDocumentFragment();
-
-			converter.bindElements( domFragment, viewFragment );
-
-			expect( converter.getCorrespondingDomDocumentFragment( viewFragment ) ).to.equal( domFragment );
-		} );
-	} );
-
-	describe( 'getCorrespondingDomText', () => {
-		it( 'should return corresponding DOM text based on sibling', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domImg );
-			domP.appendChild( domText );
-
-			const viewImg = new ViewElement( 'img' );
-
-			converter.bindElements( domImg, viewImg );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 1 );
-
-			expect( converter.getCorrespondingDomText( viewText ) ).to.equal( domText );
-		} );
-
-		it( 'should return corresponding DOM text based on parent', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 0 );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingDomText( viewText ) ).to.equal( domText );
-		} );
-
-		it( 'should return null if sibling is not bound', () => {
-			const domImg = document.createElement( 'img' );
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domImg );
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 1 );
-
-			converter.bindElements( domP, viewP );
-
-			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
-		} );
-
-		it( 'should return null if parent is not bound', () => {
-			const domText = document.createTextNode( 'foo' );
-			const domP = document.createElement( 'p' );
-
-			domP.appendChild( domText );
-
-			const viewP = converter.domToView( domP );
-			const viewText = viewP.getChild( 0 );
-
-			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
-		} );
-	} );
-} );

+ 327 - 0
packages/ckeditor5-engine/tests/treeview/domconverter/binding.js

@@ -0,0 +1,327 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, domconverter, browser-only */
+
+'use strict';
+
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
+import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import { INLINE_FILLER } from '/ckeditor5/engine/treeview/filler.js';
+
+import { parse } from '/tests/engine/_utils/view.js';
+
+import createElement from '/ckeditor5/utils/dom/createelement.js';
+
+describe( 'DomConverter', () => {
+	let converter;
+
+	before( () => {
+		converter = new DomConverter();
+	} );
+
+	describe( 'bindElements', () => {
+		it( 'should bind elements', () => {
+			const domElement = document.createElement( 'p' );
+			const viewElement = new ViewElement( 'p' );
+
+			converter.bindElements( domElement, viewElement );
+
+			expect( converter.getCorrespondingView( domElement ) ).to.equal( viewElement );
+			expect( converter.getCorrespondingDom( viewElement ) ).to.equal( domElement );
+		} );
+	} );
+
+	describe( 'bindDocumentFragments', () => {
+		it( 'should bind document fragments', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+		} );
+	} );
+
+	describe( 'getCorrespondingView', () => {
+		it( 'should return corresponding view element if element is passed', () => {
+			const domElement = document.createElement( 'p' );
+			const viewElement = new ViewElement( 'p' );
+
+			converter.bindElements( domElement, viewElement );
+
+			expect( converter.getCorrespondingView( domElement ) ).to.equal( viewElement );
+		} );
+
+		it( 'should return corresponding view text if text is passed', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = document.createElement( 'p' );
+
+			domP.appendChild( domText );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 0 );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingView( domText ) ).to.equal( viewText );
+		} );
+
+		it( 'should return corresponding view document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = converter.domToView( domFragment );
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+		} );
+
+		it( 'should return null if falsy value was passed', () => {
+			expect( converter.getCorrespondingView( null ) ).to.be.null;
+			expect( converter.getCorrespondingView( undefined ) ).to.be.null;
+		} );
+	} );
+
+	describe( 'getCorrespondingViewElement', () => {
+		it( 'should return corresponding view element', () => {
+			const domElement = document.createElement( 'p' );
+			const viewElement = new ViewElement( 'p' );
+
+			converter.bindElements( domElement, viewElement );
+
+			expect( converter.getCorrespondingViewElement( domElement ) ).to.equal( viewElement );
+		} );
+	} );
+
+	describe( 'getCorrespondingViewDocumentFragment', () => {
+		it( 'should return corresponding view document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = converter.domToView( domFragment );
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingViewDocumentFragment( domFragment ) ).to.equal( viewFragment );
+		} );
+	} );
+
+	describe( 'getCorrespondingViewText', () => {
+		it( 'should return corresponding view text based on sibling', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, [ domImg, domText ] );
+
+			const viewImg = new ViewElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 1 );
+
+			expect( converter.getCorrespondingViewText( domText ) ).to.equal( viewText );
+		} );
+
+		it( 'should return corresponding view text based on parent', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, domText );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 0 );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingViewText( domText ) ).to.equal( viewText );
+		} );
+
+		it( 'should return null if sibling is not bound', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, [ domImg, domText ] );
+
+			const viewP = converter.domToView( domP );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+		} );
+
+		it( 'should return null if sibling is not element', () => {
+			const domTextFoo = document.createTextNode( 'foo' );
+			const domTextBar = document.createTextNode( 'bar' );
+			const domP = createElement( document, 'p', null, [ domTextFoo, domTextBar ] );
+
+			const viewP = converter.domToView( domP );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingViewText( domTextBar ) ).to.be.null;
+		} );
+
+		it( 'should return null if parent is not bound', () => {
+			const domText = document.createTextNode( 'foo' );
+			createElement( document, 'p', null, domText );
+
+			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+		} );
+
+		it( 'should return null for inline filler', () => {
+			const domFiller = document.createTextNode( INLINE_FILLER );
+			const domP = createElement( document, 'p', null, domFiller );
+
+			const viewP = converter.domToView( domP );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingViewText( domFiller ) ).to.be.null;
+		} );
+
+		it( 'should return null if there is no text node sibling in view', () => {
+			const domB = document.createElement( 'b' );
+			const domI = document.createElement( 'i' );
+			const domText = document.createTextNode( 'x' );
+			const domP = createElement( document, 'p', null, [ domB, domText, domI ] );
+
+			const viewP = parse( '<p><b></b><i></i></p>' );
+			const viewB = viewP.getChild( 0 );
+			const viewI = viewP.getChild( 1 );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domI, viewI );
+			converter.bindElements( domB, viewB );
+
+			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+		} );
+
+		it( 'should return null if there is no child text node in view', () => {
+			const domText = document.createTextNode( 'x' );
+			const domP = createElement( document, 'p', null, domText );
+
+			const viewP = parse( '<p></p>' );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingViewText( domText ) ).to.be.null;
+		} );
+	} );
+
+	describe( 'getCorrespondingDom', () => {
+		it( 'should return corresponding DOM element if element was passed', () => {
+			const domElement = document.createElement( 'p' );
+			const viewElement = new ViewElement( 'p' );
+
+			converter.bindElements( domElement, viewElement );
+
+			expect( converter.getCorrespondingDom( viewElement ) ).to.equal( domElement );
+		} );
+
+		it( 'should return corresponding DOM text if text was passed', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = document.createElement( 'p' );
+
+			domP.appendChild( domText );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 0 );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingDom( viewText ) ).to.equal( domText );
+		} );
+
+		it( 'should return corresponding DOM document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+		} );
+	} );
+
+	describe( 'getCorrespondingDomElement', () => {
+		it( 'should return corresponding DOM element', () => {
+			const domElement = document.createElement( 'p' );
+			const viewElement = new ViewElement( 'p' );
+
+			converter.bindElements( domElement, viewElement );
+
+			expect( converter.getCorrespondingDomElement( viewElement ) ).to.equal( domElement );
+		} );
+	} );
+
+	describe( 'getCorrespondingDomDocumentFragment', () => {
+		it( 'should return corresponding DOM document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindElements( domFragment, viewFragment );
+
+			expect( converter.getCorrespondingDomDocumentFragment( viewFragment ) ).to.equal( domFragment );
+		} );
+	} );
+
+	describe( 'getCorrespondingDomText', () => {
+		it( 'should return corresponding DOM text based on sibling', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = document.createElement( 'p' );
+
+			domP.appendChild( domImg );
+			domP.appendChild( domText );
+
+			const viewImg = new ViewElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 1 );
+
+			expect( converter.getCorrespondingDomText( viewText ) ).to.equal( domText );
+		} );
+
+		it( 'should return corresponding DOM text based on parent', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = document.createElement( 'p' );
+
+			domP.appendChild( domText );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 0 );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingDomText( viewText ) ).to.equal( domText );
+		} );
+
+		it( 'should return null if sibling is not bound', () => {
+			const domImg = document.createElement( 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = document.createElement( 'p' );
+
+			domP.appendChild( domImg );
+			domP.appendChild( domText );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 1 );
+
+			converter.bindElements( domP, viewP );
+
+			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
+		} );
+
+		it( 'should return null if parent is not bound', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = document.createElement( 'p' );
+
+			domP.appendChild( domText );
+
+			const viewP = converter.domToView( domP );
+			const viewText = viewP.getChild( 0 );
+
+			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
+		} );
+	} );
+} );

+ 432 - 0
packages/ckeditor5-engine/tests/treeview/domconverter/dom-to-view.js

@@ -0,0 +1,432 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, domconverter, browser-only */
+
+'use strict';
+
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
+import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import { INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER } from '/ckeditor5/engine/treeview/filler.js';
+
+import { parse, stringify } from '/tests/engine/_utils/view.js';
+
+import count from '/ckeditor5/utils/count.js';
+import createElement from '/ckeditor5/utils/dom/createelement.js';
+
+describe( 'DomConverter', () => {
+	let converter;
+
+	before( () => {
+		converter = new DomConverter();
+	} );
+
+	describe( 'domToView', () => {
+		it( 'should create tree of view elements from DOM elements', () => {
+			const domImg = createElement( document, 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', { 'class': 'foo' }, [ domImg, domText ] );
+
+			const viewImg = new ViewElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const viewP = converter.domToView( domP );
+
+			expect( viewP ).to.be.an.instanceof( ViewElement );
+			expect( viewP.name ).to.equal( 'p' );
+
+			expect( viewP.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( count( viewP.getAttributeKeys() ) ).to.equal( 1 );
+
+			expect( viewP.getChildCount() ).to.equal( 2 );
+			expect( viewP.getChild( 0 ).name ).to.equal( 'img' );
+			expect( viewP.getChild( 1 ).data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
+			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domImg );
+		} );
+
+		it( 'should create tree of view elements from DOM elements and bind elements', () => {
+			const domImg = createElement( document, 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', { 'class': 'foo' }, [ domImg, domText ] );
+
+			const viewP = converter.domToView( domP, { bind: true } );
+
+			expect( viewP ).to.be.an.instanceof( ViewElement );
+			expect( viewP.name ).to.equal( 'p' );
+
+			expect( viewP.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( count( viewP.getAttributeKeys() ) ).to.equal( 1 );
+
+			expect( viewP.getChildCount() ).to.equal( 2 );
+			expect( viewP.getChild( 0 ).name ).to.equal( 'img' );
+			expect( viewP.getChild( 1 ).data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingDom( viewP ) ).to.equal( domP );
+			expect( converter.getCorrespondingDom( viewP.getChild( 0 ) ) ).to.equal( domP.childNodes[ 0 ] );
+		} );
+
+		it( 'should create tree of view elements from DOM element without children', () => {
+			const domImg = createElement( document, 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', { 'class': 'foo' }, [ domImg, domText ] );
+
+			const viewImg = new ViewElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const viewP = converter.domToView( domP, { withChildren: false } );
+
+			expect( viewP ).to.be.an.instanceof( ViewElement );
+			expect( viewP.name ).to.equal( 'p' );
+
+			expect( viewP.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( count( viewP.getAttributeKeys() ) ).to.equal( 1 );
+
+			expect( viewP.getChildCount() ).to.equal( 0 );
+			expect( converter.getCorrespondingDom( viewP ) ).to.not.equal( domP );
+		} );
+
+		it( 'should create view document fragment from DOM document fragment', () => {
+			const domImg = createElement( document, 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domFragment = document.createDocumentFragment();
+
+			domFragment.appendChild( domImg );
+			domFragment.appendChild( domText );
+
+			const viewFragment = converter.domToView( domFragment, { bind: true } );
+
+			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
+			expect( viewFragment.getChildCount() ).to.equal( 2 );
+			expect( viewFragment.getChild( 0 ).name ).to.equal( 'img' );
+			expect( viewFragment.getChild( 1 ).data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.equal( domFragment );
+			expect( converter.getCorrespondingDom( viewFragment.getChild( 0 ) ) ).to.equal( domFragment.childNodes[ 0 ] );
+		} );
+
+		it( 'should create view document fragment from DOM document fragment without children', () => {
+			const domImg = createElement( document, 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domFragment = document.createDocumentFragment();
+
+			domFragment.appendChild( domImg );
+			domFragment.appendChild( domText );
+
+			const viewImg = new ViewElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const viewFragment = converter.domToView( domFragment, { withChildren: false } );
+
+			expect( viewFragment ).to.be.an.instanceof( ViewDocumentFragment );
+
+			expect( viewFragment.getChildCount() ).to.equal( 0 );
+			expect( converter.getCorrespondingDom( viewFragment ) ).to.not.equal( domFragment );
+		} );
+
+		it( 'should return already bind document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			const viewFragment2 = converter.domToView( domFragment );
+
+			expect( viewFragment2 ).to.equal( viewFragment );
+		} );
+
+		it( 'should return null for block filler', () => {
+			const domFiller = converter.blockFiller( document );
+			const viewFiller = converter.domToView( domFiller );
+
+			expect( viewFiller ).to.be.null;
+		} );
+	} );
+
+	describe( 'domChildrenToView', () => {
+		it( 'should convert children', () => {
+			const domImg = createElement( document, 'img' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, [ domImg, domText ] );
+
+			const viewChildren = Array.from( converter.domChildrenToView( domP ) );
+
+			expect( viewChildren.length ).to.equal( 2 );
+			expect( stringify( viewChildren[ 0 ] ) ).to.equal( '<img></img>' );
+			expect( stringify( viewChildren[ 1 ] ) ).to.equal( 'foo' );
+		} );
+
+		it( 'should skip filler', () => {
+			const domFiller = converter.blockFiller( document );
+			const domP = createElement( document, 'p', null, domFiller );
+
+			const viewChildren = Array.from( converter.domChildrenToView( domP ) );
+
+			expect( viewChildren.length ).to.equal( 0 );
+		} );
+
+		it( 'should pass options', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domB = createElement( document, 'b', null, 'bar' );
+			const domP = createElement( document, 'p', null, [ domB, domText ] );
+
+			const viewChildren = Array.from( converter.domChildrenToView( domP, { withChildren: false }  ) );
+
+			expect( viewChildren.length ).to.equal( 2 );
+			expect( stringify( viewChildren[ 0 ] ) ).to.equal( '<b></b>' );
+			expect( stringify( viewChildren[ 1 ] ) ).to.equal( 'foo' );
+		} );
+	} );
+
+	describe( 'domPositionToView', () => {
+		it( 'should converter position in text', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domB = createElement( document, 'b', null, 'bar' );
+			const domP = createElement( document, 'p', null, [ domText, domB ] );
+
+			const viewP = parse( '<p>foo<b>bar</b></p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 0 ) );
+
+			const viewPosition = converter.domPositionToView( domText, 2 );
+
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>fo{}o<b>bar</b></p>' );
+		} );
+
+		it( 'should converter position in element', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domB = createElement( document, 'b', null, 'bar' );
+			const domP = createElement( document, 'p', null, [ domText, domB ] );
+
+			const viewP = parse( '<p>foo<b>bar</b></p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 0 ) );
+
+			const viewPosition = converter.domPositionToView( domP, 1 );
+
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>foo[]<b>bar</b></p>' );
+		} );
+
+		it( 'should converter position at the beginning', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, domText );
+
+			const viewP = parse( '<p>foo</p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = converter.domPositionToView( domP, 0 );
+
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>[]foo</p>' );
+		} );
+
+		it( 'should converter position inside block filler', () => {
+			const converter = new DomConverter( { blockFiller: NBSP_FILLER } );
+			const domFiller = NBSP_FILLER( document );
+			const domP = createElement( document, 'p', null, domFiller );
+
+			const viewP = parse( '<p></p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = converter.domPositionToView( domFiller, 0 );
+
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>[]</p>' );
+		} );
+
+		it( 'should converter position inside inline filler', () => {
+			const domFiller = document.createTextNode( INLINE_FILLER );
+			const domText = document.createTextNode( 'foo' );
+			const domB = createElement( document, 'b', null, domFiller );
+			const domP = createElement( document, 'p', null, [ domText, domB ] );
+
+			const viewP = parse( '<p>foo<b></b></p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 1 ) );
+
+			const viewPosition = converter.domPositionToView( domFiller, INLINE_FILLER_LENGTH );
+
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>foo<b>[]</b></p>' );
+		} );
+
+		it( 'should converter position inside inline filler with text', () => {
+			const domFiller = document.createTextNode( INLINE_FILLER + 'bar' );
+			const domText = document.createTextNode( 'foo' );
+			const domB = createElement( document, 'b', null, domFiller );
+			const domP = createElement( document, 'p', null, [ domText, domB ] );
+
+			const viewP = parse( '<p>foo<b>bar</b></p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 1 ) );
+
+			const viewPosition = converter.domPositionToView( domFiller, INLINE_FILLER_LENGTH + 2 );
+
+			expect( viewPosition.offset ).to.equal( 2 );
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>foo<b>ba{}r</b></p>' );
+		} );
+
+		it( 'should converter position inside inline filler with text at the beginning', () => {
+			const domFiller = document.createTextNode( INLINE_FILLER + 'bar' );
+			const domText = document.createTextNode( 'foo' );
+			const domB = createElement( document, 'b', null, domFiller );
+			const domP = createElement( document, 'p', null, [ domText, domB ] );
+
+			const viewP = parse( '<p>foo<b>bar</b></p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 1 ) );
+
+			const viewPosition = converter.domPositionToView( domFiller, INLINE_FILLER_LENGTH - 1 );
+
+			expect( viewPosition.offset ).to.equal( 0 );
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>foo<b>{}bar</b></p>' );
+		} );
+
+		it( 'should converter position at the end', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, domText );
+
+			const viewP = parse( '<p>foo</p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = converter.domPositionToView( domP, 1 );
+
+			expect( stringify( viewP, viewPosition ) ).to.equal( '<p>foo[]</p>' );
+		} );
+
+		it( 'should return null if there is no corresponding parent node', () => {
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, domText );
+
+			const viewPosition = converter.domPositionToView( domP, 0 );
+
+			expect( viewPosition ).to.be.null;
+		} );
+
+		it( 'should return null if there is no corresponding sibling node', () => {
+			const domB = createElement( document, 'b', null, 'bar' );
+			const domText = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, [ domB, domText ] );
+
+			const viewPosition = converter.domPositionToView( domP, 1 );
+
+			expect( viewPosition ).to.be.null;
+		} );
+
+		it( 'should return null if there is no corresponding text node', () => {
+			const domText = document.createTextNode( 'foo' );
+
+			const viewPosition = converter.domPositionToView( domText, 1 );
+
+			expect( viewPosition ).to.be.null;
+		} );
+	} );
+
+	describe( 'domRangeToView', () => {
+		it( 'should converter DOM range', () => {
+			const domFoo = document.createTextNode( 'foo' );
+			const domBar = document.createTextNode( 'bar' );
+			const domB = createElement( document, 'b', null, domBar );
+			const domP = createElement( document, 'p', null, [ domFoo, domB ] );
+
+			const viewP = parse( '<p>foo<b>bar</b></p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 1 ) );
+
+			const domRange = new Range();
+			domRange.setStart( domFoo, 1 );
+			domRange.setEnd( domBar, 2 );
+
+			const viewRange = converter.domRangeToView( domRange );
+
+			expect( stringify( viewP, viewRange ) ).to.equal( '<p>f{oo<b>ba}r</b></p>' );
+		} );
+
+		it( 'should return null if start or end is null', () => {
+			const domFoo = document.createTextNode( 'foo' );
+			const domBar = document.createTextNode( 'bar' );
+			const domB = createElement( document, 'b', null, domBar );
+			createElement( document, 'p', null, [ domFoo, domB ] );
+
+			const domRange = new Range();
+			domRange.setStart( domFoo, 1 );
+			domRange.setEnd( domBar, 2 );
+
+			const viewRange = converter.domRangeToView( domRange );
+
+			expect( viewRange ).to.be.null;
+		} );
+	} );
+
+	describe( 'domSelectionToView', () => {
+		it( 'should converter selection', () => {
+			const domFoo = document.createTextNode( 'foo' );
+			const domBar = document.createTextNode( 'bar' );
+			const domB = createElement( document, 'b', null, domBar );
+			const domP = createElement( document, 'p', null, [ domFoo, domB ] );
+
+			const viewP = parse( '<p>foo<b>bar</b></p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 1 ) );
+
+			document.body.appendChild( domP );
+
+			const domRange = new Range();
+			domRange.setStart( domFoo, 1 );
+			domRange.setEnd( domBar, 2 );
+
+			const domSelection = document.getSelection();
+			domSelection.removeAllRanges();
+			domSelection.addRange( domRange );
+
+			const viewSelection = converter.domSelectionToView( domSelection );
+
+			expect( viewSelection.rangeCount ).to.equal( 1 );
+			expect( stringify( viewP, viewSelection.getFirstRange() ) ).to.equal( '<p>f{oo<b>ba}r</b></p>' );
+		} );
+
+		it( 'should converter empty selection to empty selection', () => {
+			const domSelection = document.getSelection();
+			domSelection.removeAllRanges();
+
+			const viewSelection = converter.domSelectionToView( domSelection );
+
+			expect( viewSelection.rangeCount ).to.equal( 0 );
+		} );
+
+		it( 'should not add null ranges', () => {
+			const domFoo = document.createTextNode( 'foo' );
+			const domBar = document.createTextNode( 'bar' );
+			const domB = createElement( document, 'b', null, domBar );
+			const domP = createElement( document, 'p', null, [ domFoo, domB ] );
+
+			document.body.appendChild( domP );
+
+			const domRange = new Range();
+			domRange.setStart( domFoo, 1 );
+			domRange.setEnd( domBar, 2 );
+
+			const domSelection = document.getSelection();
+			domSelection.removeAllRanges();
+			domSelection.addRange( domRange );
+
+			const viewSelection = converter.domSelectionToView( domSelection );
+
+			expect( viewSelection.rangeCount ).to.equal( 0 );
+		} );
+	} );
+} );

+ 31 - 0
packages/ckeditor5-engine/tests/treeview/domconverter/domconverter.js

@@ -0,0 +1,31 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, domconverter */
+
+'use strict';
+
+import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+import { BR_FILLER, NBSP_FILLER } from '/ckeditor5/engine/treeview/filler.js';
+
+describe( 'DomConverter', () => {
+	let converter;
+
+	before( () => {
+		converter = new DomConverter();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create converter with BR block filler by default', () => {
+			converter = new DomConverter();
+			expect( converter.blockFiller ).to.equal( BR_FILLER );
+		} );
+
+		it( 'should create converter with defined block filler', () => {
+			converter = new DomConverter( { blockFiller: NBSP_FILLER } );
+			expect( converter.blockFiller ).to.equal( NBSP_FILLER );
+		} );
+	} );
+} );

+ 321 - 0
packages/ckeditor5-engine/tests/treeview/domconverter/view-to-dom.js

@@ -0,0 +1,321 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, domconverter, browser-only */
+
+'use strict';
+
+import ViewText from '/ckeditor5/engine/treeview/text.js';
+import ViewElement from '/ckeditor5/engine/treeview/element.js';
+import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+import ViewDocumentFragment from '/ckeditor5/engine/treeview/documentfragment.js';
+import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller } from '/ckeditor5/engine/treeview/filler.js';
+
+import { parse } from '/tests/engine/_utils/view.js';
+
+import createElement from '/ckeditor5/utils/dom/createelement.js';
+
+describe( 'DomConverter', () => {
+	let converter;
+
+	before( () => {
+		converter = new DomConverter();
+	} );
+
+	describe( 'viewToDom', () => {
+		it( 'should create tree of DOM elements from view elements', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewP = new ViewElement( 'p' );
+
+			viewP.setAttribute( 'class', 'foo' );
+
+			viewP.appendChildren( viewImg );
+			viewP.appendChildren( viewText );
+
+			const domImg = document.createElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const domP = converter.viewToDom( viewP, document );
+
+			expect( domP ).to.be.an.instanceof( HTMLElement );
+			expect( domP.tagName ).to.equal( 'P' );
+
+			expect( domP.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( domP.attributes.length ).to.equal( 1 );
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
+			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
+			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewImg );
+		} );
+
+		it( 'should create tree of DOM elements from view elements and bind elements', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewP = new ViewElement( 'p' );
+
+			viewP.setAttribute( 'class', 'foo' );
+
+			viewP.appendChildren( viewImg );
+			viewP.appendChildren( viewText );
+
+			const domP = converter.viewToDom( viewP, document, { bind: true } );
+
+			expect( domP ).to.be.an.instanceof( HTMLElement );
+			expect( domP.tagName ).to.equal( 'P' );
+
+			expect( domP.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( domP.attributes.length ).to.equal( 1 );
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
+			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingView( domP ) ).to.equal( viewP );
+			expect( converter.getCorrespondingView( domP.childNodes[ 0 ] ) ).to.equal( viewP.getChild( 0 ) );
+		} );
+
+		it( 'should create tree of DOM elements from view element without children', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewP = new ViewElement( 'p' );
+
+			viewP.setAttribute( 'class', 'foo' );
+
+			viewP.appendChildren( viewImg );
+			viewP.appendChildren( viewText );
+
+			const domImg = document.createElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const domP = converter.viewToDom( viewP, document, { withChildren: false } );
+
+			expect( domP ).to.be.an.instanceof( HTMLElement );
+			expect( domP.tagName ).to.equal( 'P' );
+
+			expect( domP.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( domP.attributes.length ).to.equal( 1 );
+
+			expect( domP.childNodes.length ).to.equal( 0 );
+			expect( converter.getCorrespondingView( domP ) ).not.to.equal( viewP );
+		} );
+
+		it( 'should create DOM document fragment from view document fragment and bind elements', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewFragment = new ViewDocumentFragment();
+
+			viewFragment.appendChildren( viewImg );
+			viewFragment.appendChildren( viewText );
+
+			const domFragment = converter.viewToDom( viewFragment, document, { bind: true } );
+
+			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
+			expect( domFragment.childNodes.length ).to.equal( 2 );
+			expect( domFragment.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
+			expect( domFragment.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			expect( converter.getCorrespondingView( domFragment ) ).to.equal( viewFragment );
+			expect( converter.getCorrespondingView( domFragment.childNodes[ 0 ] ) ).to.equal( viewFragment.getChild( 0 ) );
+		} );
+
+		it( 'should create DOM document fragment from view document without children', () => {
+			const viewImg = new ViewElement( 'img' );
+			const viewText = new ViewText( 'foo' );
+			const viewFragment = new ViewDocumentFragment();
+
+			viewFragment.appendChildren( viewImg );
+			viewFragment.appendChildren( viewText );
+
+			const domImg = document.createElement( 'img' );
+
+			converter.bindElements( domImg, viewImg );
+
+			const domFragment = converter.viewToDom( viewFragment, document, { withChildren: false } );
+
+			expect( domFragment ).to.be.an.instanceof( DocumentFragment );
+
+			expect( domFragment.childNodes.length ).to.equal( 0 );
+			expect( converter.getCorrespondingView( domFragment ) ).not.to.equal( viewFragment );
+		} );
+
+		it( 'should return already bind document fragment', () => {
+			const domFragment = document.createDocumentFragment();
+			const viewFragment = new ViewDocumentFragment();
+
+			converter.bindDocumentFragments( domFragment, viewFragment );
+
+			const domFragment2 = converter.viewToDom( viewFragment );
+
+			expect( domFragment2 ).to.equal( domFragment );
+		} );
+	} );
+
+	describe( 'viewChildrenToDom', () => {
+		it( 'should convert children', () => {
+			const viewP = parse( '<container:p>foo<attribute:b>bar</attribute:b></container:p>' );
+
+			const domChildren = Array.from( converter.viewChildrenToDom( viewP, document ) );
+
+			expect( domChildren.length ).to.equal( 2 );
+			expect( domChildren[ 0 ].data ).to.equal( 'foo' );
+			expect( domChildren[ 1 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domChildren[ 1 ].childNodes.length ).to.equal( 1 );
+		} );
+
+		it( 'should add filler', () => {
+			const viewP = parse( '<container:p></container:p>' );
+
+			const domChildren = Array.from( converter.viewChildrenToDom( viewP, document ) );
+
+			expect( domChildren.length ).to.equal( 1 );
+			expect( isBlockFiller( domChildren[ 0 ], converter.blockFiller ) ).to.be.true;
+		} );
+
+		it( 'should add filler according to fillerPositionOffset', () => {
+			const viewP = parse( '<container:p>foo</container:p>' );
+			viewP.getFillerOffset = () => 0;
+
+			const domChildren = Array.from( converter.viewChildrenToDom( viewP, document ) );
+
+			expect( domChildren.length ).to.equal( 2 );
+			expect( isBlockFiller( domChildren[ 0 ], converter.blockFiller ) ).to.be.true;
+			expect( domChildren[ 1 ].data ).to.equal( 'foo' );
+		} );
+
+		it( 'should pass options', () => {
+			const viewP = parse( '<container:p>foo<attribute:b>bar</attribute:b></container:p>' );
+
+			const domChildren = Array.from( converter.viewChildrenToDom( viewP, document, { withChildren: false } ) );
+
+			expect( domChildren.length ).to.equal( 2 );
+			expect( domChildren[ 0 ].data ).to.equal( 'foo' );
+			expect( domChildren[ 1 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domChildren[ 1 ].childNodes.length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'viewPositionToDom', () => {
+		it( 'should convert the position in the text', () => {
+			const domFoo = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, domFoo );
+			const { view: viewP, selection } = parse( '<container:p>fo{}o</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( 2 );
+			expect( domPosition.parent ).to.equal( domFoo );
+		} );
+
+		it( 'should convert the position in the empty element', () => {
+			const domP = createElement( document, 'p' );
+			const { view: viewP, selection } = parse( '<container:p>[]</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( 0 );
+			expect( domPosition.parent ).to.equal( domP );
+		} );
+
+		it( 'should convert the position in the non-empty element', () => {
+			const domB = createElement( document, 'b', null, 'foo' );
+			const domP = createElement( document, 'p', null, domB );
+			const { view: viewP, selection } = parse( '<container:p><attribute:b>foo</attribute:b>[]</container:p>' );
+
+			converter.bindElements( domP, viewP );
+			converter.bindElements( domB, viewP.getChild( 0 ) );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( 1 );
+			expect( domPosition.parent ).to.equal( domP );
+		} );
+
+		it( 'should convert the position after text', () => {
+			const domP = createElement( document, 'p', null, 'foo' );
+			const { view: viewP, selection } = parse( '<container:p>foo[]</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( 1 );
+			expect( domPosition.parent ).to.equal( domP );
+		} );
+
+		it( 'should convert the position before text', () => {
+			const domP = createElement( document, 'p', null, 'foo' );
+			const { view: viewP, selection } = parse( '<container:p>[]foo</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( 0 );
+			expect( domPosition.parent ).to.equal( domP );
+		} );
+
+		it( 'should update offset if DOM text node starts with inline filler', () => {
+			const domFoo = document.createTextNode( INLINE_FILLER + 'foo' );
+			const domP = createElement( document, 'p', null, domFoo );
+			const { view: viewP, selection } = parse( '<container:p>fo{}o</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( INLINE_FILLER_LENGTH + 2 );
+			expect( domPosition.parent ).to.equal( domFoo );
+		} );
+
+		it( 'should move the position to the text node if the position is where inline filler is', () => {
+			const domFiller = document.createTextNode( INLINE_FILLER );
+			const domP = createElement( document, 'p', null, domFiller );
+			const { view: viewP, selection } = parse( '<container:p>[]</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewPosition = selection.getFirstPosition();
+			const domPosition = converter.viewPositionToDom( viewPosition );
+
+			expect( domPosition.offset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domPosition.parent ).to.equal( domFiller );
+		} );
+	} );
+
+	describe( 'viewRangeToDom', () => {
+		it( 'should convert view range to DOM range', () => {
+			const domFoo = document.createTextNode( 'foo' );
+			const domP = createElement( document, 'p', null, domFoo );
+			const { view: viewP, selection } = parse( '<container:p>fo{o]</container:p>' );
+
+			converter.bindElements( domP, viewP );
+
+			const viewRange = selection.getFirstRange();
+			const domRange = converter.viewRangeToDom( viewRange );
+
+			expect( domRange ).to.be.instanceof( Range );
+			expect( domRange.startContainer ).to.equal( domFoo );
+			expect( domRange.startOffset ).to.equal( 2 );
+			expect( domRange.endContainer ).to.equal( domP );
+			expect( domRange.endOffset ).to.equal( 1 );
+		} );
+	} );
+} );

+ 134 - 0
packages/ckeditor5-engine/tests/treeview/filler.js

@@ -0,0 +1,134 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, browser-only */
+
+'use strict';
+
+import {
+	BR_FILLER,
+	NBSP_FILLER,
+	INLINE_FILLER_LENGTH,
+	INLINE_FILLER,
+	startsWithFiller,
+	isInlineFiller,
+	getDataWithoutFiller,
+	isBlockFiller
+} from '/ckeditor5/engine/treeview/filler.js';
+
+describe( 'filler', () => {
+	describe( 'INLINE_FILLER', () => {
+		it( 'should have length equal INLINE_FILLER_LENGTH', () => {
+			expect( INLINE_FILLER.length ).to.equal( INLINE_FILLER_LENGTH );
+		} );
+	} );
+
+	describe( 'startsWithFiller', () => {
+		it( 'should be true for element which contains only filler', () => {
+			const node = document.createTextNode( INLINE_FILLER );
+
+			expect( startsWithFiller( node ) ).to.be.true;
+		} );
+
+		it( 'should be true for element which starts with filler', () => {
+			const node = document.createTextNode( INLINE_FILLER + 'foo' );
+
+			expect( startsWithFiller( node ) ).to.be.true;
+		} );
+
+		it( 'should be false which contains filler in the middle', () => {
+			const node = document.createTextNode( 'x' + INLINE_FILLER + 'x' );
+
+			expect( startsWithFiller( node ) ).to.be.false;
+		} );
+
+		it( 'should be false for the node which does not contains filler', () => {
+			const node = document.createTextNode( 'foo' );
+
+			expect( startsWithFiller( node ) ).to.be.false;
+		} );
+
+		it( 'should be false for the node which does not contains filler, even if it has the same length', () => {
+			let text = '';
+
+			for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
+				text += 'x';
+			}
+
+			const node = document.createTextNode( text );
+
+			expect( startsWithFiller( node ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'getDataWithoutFiller', () => {
+		it( 'should return data without filler', () => {
+			const node = document.createTextNode( INLINE_FILLER + 'foo' );
+
+			const dataWithoutFiller = getDataWithoutFiller( node );
+
+			expect( dataWithoutFiller.length ).to.equals( 3 );
+			expect( dataWithoutFiller ).to.equals( 'foo' );
+		} );
+
+		it( 'should return the same data for data without filler', () => {
+			const node = document.createTextNode( 'foo' );
+
+			const dataWithoutFiller = getDataWithoutFiller( node );
+
+			expect( dataWithoutFiller.length ).to.equals( 3 );
+			expect( dataWithoutFiller ).to.equals( 'foo' );
+		} );
+	} );
+
+	describe( 'isInlineFiller', () => {
+		it( 'should be true for inline filler', () => {
+			const node = document.createTextNode( INLINE_FILLER );
+
+			expect( isInlineFiller( node ) ).to.be.true;
+		} );
+
+		it( 'should be false for element which starts with filler', () => {
+			const node = document.createTextNode( INLINE_FILLER + 'foo' );
+
+			expect( isInlineFiller( node ) ).to.be.false;
+		} );
+
+		it( 'should be false for the node which does not contains filler, even if it has the same length', () => {
+			let text = '';
+
+			for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
+				text += 'x';
+			}
+
+			const node = document.createTextNode( text );
+
+			expect( isInlineFiller( node ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'isBlockFiller', () => {
+		it( 'should return true if the node is an instance of the BR block filler', () => {
+			const brFillerInstance = BR_FILLER( document );
+
+			expect( isBlockFiller( brFillerInstance, BR_FILLER ) ).to.be.true;
+			// Check it twice to ensure that caching breaks nothing.
+			expect( isBlockFiller( brFillerInstance, BR_FILLER ) ).to.be.true;
+		} );
+
+		it( 'should return true if the node is an instance of the NBSP block filler', () => {
+			const nbspFillerInstance = NBSP_FILLER( document );
+
+			expect( isBlockFiller( nbspFillerInstance, NBSP_FILLER ) ).to.be.true;
+			// Check it twice to ensure that caching breaks nothing.
+			expect( isBlockFiller( nbspFillerInstance, NBSP_FILLER ) ).to.be.true;
+		} );
+
+		it( 'should return false for inline filler', () => {
+			expect( isBlockFiller( document.createTextNode( INLINE_FILLER ), BR_FILLER ) ).to.be.false;
+			expect( isBlockFiller( document.createTextNode( INLINE_FILLER ), NBSP_FILLER ) ).to.be.false;
+		} );
+	} );
+} );

+ 0 - 0
packages/ckeditor5-engine/tests/treeview/manual/typing.html → packages/ckeditor5-engine/tests/treeview/manual/__template__.html


+ 11 - 11
packages/ckeditor5-engine/tests/treeview/manual/clickobserver.js

@@ -7,15 +7,15 @@
 
 'use strict';
 
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
+import Document from '/ckeditor5/engine/treeview/document.js';
 import DomEventObserver from '/ckeditor5/engine/treeview/observer/domeventobserver.js';
 
-const treeView = new TreeView();
+const viewDocument = new Document();
 let observer1, observer2;
 
 class ClickObserver1 extends DomEventObserver {
-	constructor( treeView ) {
-		super( treeView );
+	constructor( viewDocument ) {
+		super( viewDocument );
 
 		this.id = 1;
 		this.domEventType = 'click';
@@ -28,8 +28,8 @@ class ClickObserver1 extends DomEventObserver {
 }
 
 class ClickObserver2 extends DomEventObserver {
-	constructor( treeView ) {
-		super( treeView );
+	constructor( viewDocument ) {
+		super( viewDocument );
 
 		this.id = 2;
 		this.domEventType = 'click';
@@ -41,15 +41,15 @@ class ClickObserver2 extends DomEventObserver {
 	}
 }
 
-treeView.on( 'click', ( evt, eventId, elementId ) => console.log( 'click', eventId, elementId ) );
+viewDocument.on( 'click', ( evt, eventId, elementId ) => console.log( 'click', eventId, elementId ) );
 document.getElementById( 'enable1' ).addEventListener( 'click', () => observer1.enable() );
 document.getElementById( 'disable1' ).addEventListener( 'click', () => observer1.disable() );
 
 // Random order.
-treeView.addObserver( ClickObserver1 );
+viewDocument.addObserver( ClickObserver1 );
 
-treeView.createRoot( document.getElementById( 'clickerA' ), 'clickerA' );
+viewDocument.createRoot( document.getElementById( 'clickerA' ), 'clickerA' );
 
-treeView.addObserver( ClickObserver2 );
+viewDocument.addObserver( ClickObserver2 );
 
-treeView.createRoot( document.getElementById( 'clickerB' ), 'clickerB' );
+viewDocument.createRoot( document.getElementById( 'clickerB' ), 'clickerB' );

+ 7 - 7
packages/ckeditor5-engine/tests/treeview/manual/focusobserver.js

@@ -7,15 +7,15 @@
 
 'use strict';
 
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
+import Document from '/ckeditor5/engine/treeview/document.js';
 import FocusObserver from '/ckeditor5/engine/treeview/observer/focusobserver.js';
 
-const treeView = new TreeView();
+const viewDocument = new Document();
 
-treeView.on( 'focus', ( evt, data ) => console.log( 'focus', data.domTarget ) );
-treeView.on( 'blur', ( evt, data ) => console.log( 'blur', data.domTarget ) );
+viewDocument.on( 'focus', ( evt, data ) => console.log( 'focus', data.domTarget ) );
+viewDocument.on( 'blur', ( evt, data ) => console.log( 'blur', data.domTarget ) );
 
-treeView.addObserver( FocusObserver );
+viewDocument.addObserver( FocusObserver );
 
-treeView.createRoot( document.getElementById( 'editable1' ), 'editable1' );
-treeView.createRoot( document.getElementById( 'editable2' ), 'editable2' );
+viewDocument.createRoot( document.getElementById( 'editable1' ), 'editable1' );
+viewDocument.createRoot( document.getElementById( 'editable2' ), 'editable2' );

+ 27 - 0
packages/ckeditor5-engine/tests/treeview/manual/immutable.js

@@ -0,0 +1,27 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/treeview/document.js';
+import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
+import SelectionObserver from '/ckeditor5/engine/treeview/observer/selectionobserver.js';
+import KeyObserver from '/ckeditor5/engine/treeview/observer/keyobserver.js';
+import { setData } from '/tests/engine/_utils/view.js';
+
+const viewDocument = new Document();
+viewDocument.createRoot( document.getElementById( 'editor' ) );
+
+viewDocument.addObserver( MutationObserver );
+viewDocument.addObserver( SelectionObserver );
+viewDocument.addObserver( KeyObserver );
+
+setData( viewDocument,
+	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' +
+	'<container:p></container:p>' +
+	'<container:p><attribute:strong></attribute:strong></container:p>' +
+	'<container:p>bom</container:p>' );
+
+viewDocument.render();

+ 4 - 0
packages/ckeditor5-engine/tests/treeview/manual/immutable.md

@@ -0,0 +1,4 @@
+@bender-ui: collapsed
+@bender-tags: treeview
+
+It should not be possible to move selection nor modify document.

+ 0 - 4
packages/ckeditor5-engine/tests/treeview/manual/init.html

@@ -1,4 +0,0 @@
-<div contenteditable="true" id="editor">
-	<p>Old</p>
-	<p>Content</p>
-</div>

+ 0 - 24
packages/ckeditor5-engine/tests/treeview/manual/init.js

@@ -1,24 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global console:false */
-
-'use strict';
-
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
-import Element from '/ckeditor5/engine/treeview/element.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
-
-const treeView = new TreeView();
-treeView.createRoot( document.getElementById( 'editor' ), 'editor' );
-
-treeView.viewRoots.get( 'editor' ).appendChildren( [
-	new Element( 'p', [], [ new Text( 'New' ) ] ),
-	new Element( 'p', [], [ new Text( 'Content' ) ] )
-] );
-
-treeView.render();
-
-console.log( treeView );

+ 0 - 13
packages/ckeditor5-engine/tests/treeview/manual/init.md

@@ -1,13 +0,0 @@
-@bender-ui: collapsed
-@bender-tags: treeview
-
-## Init ##
-
-TreeView should be initialised and displayed in console.
-
-The content shoud be:
-
-```
-New
-Content
-```

+ 28 - 0
packages/ckeditor5-engine/tests/treeview/manual/inline-filler.js

@@ -0,0 +1,28 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/treeview/document.js';
+import SelectionObserver from '/ckeditor5/engine/treeview/observer/selectionobserver.js';
+import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
+import KeyObserver from '/ckeditor5/engine/treeview/observer/keyobserver.js';
+import { setData } from '/tests/engine/_utils/view.js';
+
+const viewDocument = new Document();
+viewDocument.createRoot( document.getElementById( 'editor' ) );
+
+viewDocument.addObserver( MutationObserver );
+viewDocument.addObserver( SelectionObserver );
+viewDocument.addObserver( KeyObserver );
+
+setData( viewDocument,
+	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' );
+
+viewDocument.on( 'selectionChange', ( evt, data ) => {
+	viewDocument.selection.setTo( data.newSelection );
+} );
+
+viewDocument.render();

+ 8 - 0
packages/ckeditor5-engine/tests/treeview/manual/inline-filler.md

@@ -0,0 +1,8 @@
+@bender-ui: collapsed
+@bender-tags: treeview
+
+ * initialization expected: `foo{}bar`
+ * press arrow left,
+ * expected: `fo{}obar`
+ * press 2 times arrow right,
+ * expected: `foob{}ar`

+ 5 - 5
packages/ckeditor5-engine/tests/treeview/manual/keyobserver.js

@@ -7,13 +7,13 @@
 
 'use strict';
 
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
+import Document from '/ckeditor5/engine/treeview/document.js';
 import KeyObserver from '/ckeditor5/engine/treeview/observer/keyobserver.js';
 
-const treeView = new TreeView();
+const viewDocument = new Document();
 
-treeView.on( 'keydown', ( evt, data ) => console.log( 'keydown', data ) );
+viewDocument.on( 'keydown', ( evt, data ) => console.log( 'keydown', data ) );
 
-treeView.addObserver( KeyObserver );
+viewDocument.addObserver( KeyObserver );
 
-treeView.createRoot( document.getElementById( 'editable' ), 'editable' );
+viewDocument.createRoot( document.getElementById( 'editable' ), 'editable' );

+ 10 - 12
packages/ckeditor5-engine/tests/treeview/manual/mutationobserver.js

@@ -7,21 +7,19 @@
 
 'use strict';
 
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
-import Element from '/ckeditor5/engine/treeview/element.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
+import Document from '/ckeditor5/engine/treeview/document.js';
 import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
+import { setData } from '/tests/engine/_utils/view.js';
 
-const treeView = new TreeView();
-treeView.createRoot( document.getElementById( 'editor' ), 'editor' );
+const viewDocument = new Document();
+viewDocument.createRoot( document.getElementById( 'editor' ) );
 
-treeView.on( 'mutations', ( evt, mutations ) => console.log( mutations ) );
+viewDocument.on( 'mutations', ( evt, mutations ) => console.log( mutations ) );
 
-treeView.addObserver( MutationObserver );
+viewDocument.addObserver( MutationObserver );
 
-treeView.viewRoots.get( 'editor' ).appendChildren( [
-	new Element( 'p', [], [ new Text( 'foo' ) ] ),
-	new Element( 'p', [], [ new Text( 'bom' ) ] )
-] );
+setData( viewDocument,
+	'<container:p>foo</container:p>' +
+	'<container:p>bar</container:p>' );
 
-treeView.render();
+viewDocument.render();

+ 30 - 0
packages/ckeditor5-engine/tests/treeview/manual/selectionobserver.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global console:false */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/treeview/document.js';
+import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
+import SelectionObserver from '/ckeditor5/engine/treeview/observer/selectionobserver.js';
+import { setData } from '/tests/engine/_utils/view.js';
+
+const viewDocument = new Document();
+viewDocument.createRoot( document.getElementById( 'editor' ) );
+
+viewDocument.addObserver( MutationObserver );
+viewDocument.addObserver( SelectionObserver );
+
+setData( viewDocument,
+	'<container:p><attribute:b>foo</attribute:b>bar</container:p>' +
+	'<container:p>bom</container:p>' );
+
+viewDocument.on( 'selectionChange', ( evt, data ) => {
+	console.log( data );
+	viewDocument.selection.setTo( data.newSelection );
+} );
+
+viewDocument.render();

+ 10 - 0
packages/ckeditor5-engine/tests/treeview/manual/selectionobserver.md

@@ -0,0 +1,10 @@
+@bender-ui: collapsed
+@bender-tags: treeview
+
+### Move selection
+
+Selection should be moved and event should be fired (see console),
+
+### Change document
+
+Nothing should happen (no change nor event).

+ 0 - 35
packages/ckeditor5-engine/tests/treeview/manual/typing.js

@@ -1,35 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* global console:false */
-
-'use strict';
-
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
-import Element from '/ckeditor5/engine/treeview/element.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
-import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
-
-const treeView = new TreeView();
-treeView.createRoot( document.getElementById( 'editor' ), 'editor' );
-
-treeView.on( 'mutations', ( evt, mutations ) => console.log( mutations ) );
-treeView.on( 'mutations', handleTyping );
-
-treeView.addObserver( MutationObserver );
-
-treeView.viewRoots.get( 'editor' ).appendChildren( [ new Element( 'p', [], [ new Text( 'foo' ) ] ) ] );
-
-treeView.render();
-
-function handleTyping( evt, mutations ) {
-	const mutation = mutations[ 0 ];
-
-	if ( mutations.length > 1 || mutation.type !== 'text' ) {
-		return;
-	}
-
-	mutation.node.data = mutation.newText;
-}

+ 0 - 8
packages/ckeditor5-engine/tests/treeview/manual/typing.md

@@ -1,8 +0,0 @@
-@bender-ui: collapsed
-@bender-tags: treeview
-
-## Typing ##
-
-* It should be possible to type (change the content of the text node).
-* All other operations should be permitted, it should not be possible to remove whole node.
-* Composition should work fine, not be broken.

+ 33 - 0
packages/ckeditor5-engine/tests/treeview/manual/x-index.js

@@ -0,0 +1,33 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global console:false */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/treeview/document.js';
+import SelectionObserver from '/ckeditor5/engine/treeview/observer/selectionobserver.js';
+import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
+import { setData } from '/tests/engine/_utils/view.js';
+
+const viewDocument = new Document();
+viewDocument.createRoot( document.getElementById( 'editor' ) );
+
+viewDocument.addObserver( MutationObserver );
+viewDocument.addObserver( SelectionObserver );
+
+setData( viewDocument,
+	'<container:p>fo{}o</container:p>' +
+	'<container:p></container:p>' +
+	'<container:p><attribute:strong></attribute:strong></container:p>' +
+	'<container:p>bar</container:p>' );
+
+viewDocument.on( 'selectionChange', ( evt, data ) => {
+	const node = data.newSelection.getFirstPosition().parent;
+	console.log( node.name ? node.name : node._data );
+	viewDocument.selection.setTo( data.newSelection );
+} );
+
+viewDocument.render();

+ 13 - 0
packages/ckeditor5-engine/tests/treeview/manual/x-index.md

@@ -0,0 +1,13 @@
+@bender-ui: collapsed
+@bender-tags: treeview
+
+ * initialization expected: `fo{}o`
+ * press down arrow 3 times
+ * caret should be at: `ba{}r`
+ * expected console output:
+
+```
+p
+strong
+bar
+```

+ 10 - 10
packages/ckeditor5-engine/tests/treeview/node.js

@@ -128,31 +128,31 @@ describe( 'Node', () => {
 		} );
 	} );
 
-	describe( 'getTreeView', () => {
-		it( 'should return null if any parent has not set treeview', () => {
-			expect( charA.getTreeView() ).to.be.null;
+	describe( 'getDocument', () => {
+		it( 'should return null if any parent has not set Document', () => {
+			expect( charA.getDocument() ).to.be.null;
 		} );
 
 		it( 'should return TreeView attached to the element', () => {
 			const tvMock = {};
 			const element = new Element( 'p' );
 
-			element.setTreeView( tvMock );
+			element.setDocument( tvMock );
 
-			expect( element.getTreeView() ).to.equal( tvMock );
+			expect( element.getDocument() ).to.equal( tvMock );
 		} );
 
-		it( 'should return TreeView attached to the parent element', () => {
-			const tvMock = {};
+		it( 'should return Document attached to the parent element', () => {
+			const docMock = {};
 			const parent = new Element( 'div' );
 			const child = new Element( 'p' );
 
 			child.parent = parent;
 
-			parent.setTreeView( tvMock );
+			parent.setDocument( docMock );
 
-			expect( parent.getTreeView() ).to.equal( tvMock );
-			expect( child.getTreeView() ).to.equal( tvMock );
+			expect( parent.getDocument() ).to.equal( docMock );
+			expect( child.getDocument() ).to.equal( docMock );
 		} );
 	} );
 

+ 2 - 0
packages/ckeditor5-engine/tests/treeview/observer/__template__.html

@@ -0,0 +1,2 @@
+<div contenteditable="true" id="main"></div>
+<div contenteditable="true" id="additional"></div>

+ 10 - 10
packages/ckeditor5-engine/tests/treeview/observer/domeventdata.js

@@ -3,28 +3,28 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
 import DomEventData from '/ckeditor5/engine/treeview/observer/domeventdata.js';
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
+import ViewDocument from '/ckeditor5/engine/treeview/document.js';
 
 describe( 'DomEventData', () => {
-	let treeView, viewBody;
+	let viewDocument, viewBody;
 
 	beforeEach( () => {
-		treeView = new TreeView();
+		viewDocument = new ViewDocument();
 
-		viewBody = treeView.domConverter.domToView( document.body, { bind: true } );
+		viewBody = viewDocument.domConverter.domToView( document.body, { bind: true } );
 	} );
 
 	describe( 'constructor', () => {
 		it( 'sets properties', () => {
 			const domEvt = { target: document.body };
-			const data = new DomEventData( treeView, domEvt, { foo: 1, bar: true } );
+			const data = new DomEventData( viewDocument, domEvt, { foo: 1, bar: true } );
 
-			expect( data ).to.have.property( 'treeView', treeView );
+			expect( data ).to.have.property( 'document', viewDocument );
 			expect( data ).to.have.property( 'domEvent', domEvt );
 			expect( data ).to.have.property( 'domTarget', document.body );
 
@@ -36,7 +36,7 @@ describe( 'DomEventData', () => {
 	describe( 'target', () => {
 		it( 'returns bound element', () => {
 			const domEvt = { target: document.body };
-			const data = new DomEventData( treeView, domEvt );
+			const data = new DomEventData( viewDocument, domEvt );
 
 			expect( data ).to.have.property( 'target', viewBody );
 		} );
@@ -45,7 +45,7 @@ describe( 'DomEventData', () => {
 	describe( 'preventDefault', () => {
 		it( 'executes native preventDefault()', () => {
 			const domEvt = { target: document.body, preventDefault: sinon.spy() };
-			const data = new DomEventData( treeView, domEvt );
+			const data = new DomEventData( viewDocument, domEvt );
 
 			data.preventDefault();
 
@@ -56,7 +56,7 @@ describe( 'DomEventData', () => {
 	describe( 'stopPropagation', () => {
 		it( 'executes native stopPropagation()', () => {
 			const domEvt = { target: document.body, stopPropagation: sinon.spy() };
-			const data = new DomEventData( treeView, domEvt );
+			const data = new DomEventData( viewDocument, domEvt );
 
 			data.stopPropagation();
 

+ 25 - 28
packages/ckeditor5-engine/tests/treeview/observer/domeventobserver.js

@@ -3,17 +3,17 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
 import DomEventObserver from '/ckeditor5/engine/treeview/observer/domeventobserver.js';
 import Observer from '/ckeditor5/engine/treeview/observer/observer.js';
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
+import ViewDocument from '/ckeditor5/engine/treeview/document.js';
 
 class ClickObserver extends DomEventObserver {
-	constructor( treeView ) {
-		super( treeView );
+	constructor( document ) {
+		super( document );
 
 		this.domEventType = 'click';
 	}
@@ -24,8 +24,8 @@ class ClickObserver extends DomEventObserver {
 }
 
 class MultiObserver extends DomEventObserver {
-	constructor( treeView ) {
-		super( treeView );
+	constructor( document ) {
+		super( document );
 
 		this.domEventType = [ 'evt1', 'evt2' ];
 	}
@@ -36,15 +36,15 @@ class MultiObserver extends DomEventObserver {
 }
 
 describe( 'DomEventObserver', () => {
-	let treeView;
+	let viewDocument;
 
 	beforeEach( () => {
-		treeView = new TreeView();
+		viewDocument = new ViewDocument();
 	} );
 
 	describe( 'constructor', () => {
 		it( 'should create Observer with properties', () => {
-			const observer = new DomEventObserver( treeView );
+			const observer = new DomEventObserver( viewDocument );
 
 			expect( observer ).to.be.an.instanceof( Observer );
 		} );
@@ -55,9 +55,9 @@ describe( 'DomEventObserver', () => {
 		const domEvent = new MouseEvent( 'click' );
 		const evtSpy = sinon.spy();
 
-		treeView.createRoot( domElement, 'root' );
-		treeView.addObserver( ClickObserver );
-		treeView.on( 'click', evtSpy );
+		viewDocument.createRoot( domElement, 'root' );
+		viewDocument.addObserver( ClickObserver );
+		viewDocument.on( 'click', evtSpy );
 
 		domElement.dispatchEvent( domEvent );
 
@@ -77,10 +77,10 @@ describe( 'DomEventObserver', () => {
 		const evtSpy1 = sinon.spy();
 		const evtSpy2 = sinon.spy();
 
-		treeView.createRoot( domElement, 'root' );
-		treeView.addObserver( MultiObserver );
-		treeView.on( 'evt1', evtSpy1 );
-		treeView.on( 'evt2', evtSpy2 );
+		viewDocument.createRoot( domElement, 'root' );
+		viewDocument.addObserver( MultiObserver );
+		viewDocument.on( 'evt1', evtSpy1 );
+		viewDocument.on( 'evt2', evtSpy2 );
 
 		domElement.dispatchEvent( domEvent1 );
 		expect( evtSpy1.calledOnce ).to.be.true;
@@ -94,11 +94,10 @@ describe( 'DomEventObserver', () => {
 		const domEvent = new MouseEvent( 'click' );
 		const evtSpy = sinon.spy();
 
-		treeView.createRoot( domElement, 'root' );
-		treeView.addObserver( ClickObserver );
-		treeView.on( 'click', evtSpy );
+		viewDocument.createRoot( domElement, 'root' );
+		const testObserver = viewDocument.addObserver( ClickObserver );
+		viewDocument.on( 'click', evtSpy );
 
-		const testObserver = Array.from( treeView._observers )[ 0 ];
 		testObserver.disable();
 
 		domElement.dispatchEvent( domEvent );
@@ -109,14 +108,12 @@ describe( 'DomEventObserver', () => {
 	it( 'should fire event if observer is disabled and re-enabled', () => {
 		const domElement = document.createElement( 'p' );
 		const domEvent = new MouseEvent( 'click' );
-		const treeView = new TreeView();
+		const viewDocument = new ViewDocument();
 		const evtSpy = sinon.spy();
 
-		treeView.createRoot( domElement, 'root' );
-		treeView.addObserver( ClickObserver );
-		treeView.on( 'click', evtSpy );
-
-		const testObserver = Array.from( treeView._observers )[ 0 ];
+		viewDocument.createRoot( domElement, 'root' );
+		const testObserver = viewDocument.addObserver( ClickObserver );
+		viewDocument.on( 'click', evtSpy );
 
 		testObserver.disable();
 
@@ -133,8 +130,8 @@ describe( 'DomEventObserver', () => {
 
 	describe( 'fire', () => {
 		it( 'should do nothing if observer is disabled', () => {
-			const testObserver = new ClickObserver( treeView );
-			const fireSpy = sinon.spy( treeView, 'fire' );
+			const testObserver = new ClickObserver( viewDocument );
+			const fireSpy = sinon.spy( viewDocument, 'fire' );
 
 			testObserver.disable();
 

+ 7 - 9
packages/ckeditor5-engine/tests/treeview/observer/focusobserver.js

@@ -3,21 +3,19 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
 import FocusObserver from '/ckeditor5/engine/treeview/observer/focusobserver.js';
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
+import ViewDocument from '/ckeditor5/engine/treeview/document.js';
 
 describe( 'FocusObserver', () => {
-	let treeView, observer;
+	let viewDocument, observer;
 
 	beforeEach( () => {
-		treeView = new TreeView();
-		treeView.addObserver( FocusObserver );
-
-		observer = Array.from( treeView._observers )[ 0 ];
+		viewDocument = new ViewDocument();
+		observer = viewDocument.addObserver( FocusObserver );
 	} );
 
 	it( 'should define domEventType', () => {
@@ -28,7 +26,7 @@ describe( 'FocusObserver', () => {
 		it( 'should fire focus with the right event data', () => {
 			const spy = sinon.spy();
 
-			treeView.on( 'focus', spy );
+			viewDocument.on( 'focus', spy );
 
 			observer.onDomEvent( { type: 'focus', target: document.body } );
 
@@ -41,7 +39,7 @@ describe( 'FocusObserver', () => {
 		it( 'should fire blur with the right event data', () => {
 			const spy = sinon.spy();
 
-			treeView.on( 'blur', spy );
+			viewDocument.on( 'blur', spy );
 
 			observer.onDomEvent( { type: 'blur', target: document.body } );
 

+ 8 - 10
packages/ckeditor5-engine/tests/treeview/observer/keyobserver.js

@@ -3,22 +3,20 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
 import KeyObserver from '/ckeditor5/engine/treeview/observer/keyobserver.js';
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
+import ViewDocument from '/ckeditor5/engine/treeview/document.js';
 import { getCode } from '/ckeditor5/utils/keyboard.js';
 
 describe( 'KeyObserver', () => {
-	let treeView, observer;
+	let viewDocument, observer;
 
 	beforeEach( () => {
-		treeView = new TreeView();
-		treeView.addObserver( KeyObserver );
-
-		observer = Array.from( treeView._observers )[ 0 ];
+		viewDocument = new ViewDocument();
+		observer = viewDocument.addObserver( KeyObserver );
 	} );
 
 	it( 'should define domEventType', () => {
@@ -29,7 +27,7 @@ describe( 'KeyObserver', () => {
 		it( 'should fire keydown with the target and key info', () => {
 			const spy = sinon.spy();
 
-			treeView.on( 'keydown', spy );
+			viewDocument.on( 'keydown', spy );
 
 			observer.onDomEvent( { target: document.body, keyCode: 111, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false } );
 
@@ -50,7 +48,7 @@ describe( 'KeyObserver', () => {
 		it( 'should fire keydown with proper key modifiers info', () => {
 			const spy = sinon.spy();
 
-			treeView.on( 'keydown', spy );
+			viewDocument.on( 'keydown', spy );
 
 			observer.onDomEvent( { target: document.body, keyCode: 111, altKey: true, ctrlKey: true, metaKey: false, shiftKey: true } );
 
@@ -68,7 +66,7 @@ describe( 'KeyObserver', () => {
 		it( 'should fire keydown ctrlKey set to true one meta (cmd) was pressed', () => {
 			const spy = sinon.spy();
 
-			treeView.on( 'keydown', spy );
+			viewDocument.on( 'keydown', spy );
 
 			observer.onDomEvent( { target: document.body, keyCode: 111, metaKey: true } );
 

+ 0 - 2
packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.html

@@ -1,2 +0,0 @@
-<div contenteditable="true" id="editor"></div>
-<div contenteditable="true" id="editor2"></div>

+ 96 - 35
packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.js

@@ -3,38 +3,35 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
-import Element from '/ckeditor5/engine/treeview/element.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
+import ViewDocument from '/ckeditor5/engine/treeview/document.js';
 import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
+import { parse } from '/tests/engine/_utils/view.js';
 
 describe( 'MutationObserver', () => {
-	let domEditor, treeView, viewRoot, mutationObserver, lastMutations;
+	let domEditor, viewDocument, viewRoot, mutationObserver, lastMutations;
 
 	beforeEach( () => {
-		treeView = new TreeView();
-		domEditor = document.getElementById( 'editor' );
+		viewDocument = new ViewDocument();
+		domEditor = document.getElementById( 'main' );
 		lastMutations = null;
 
-		treeView.createRoot( domEditor, 'editor' );
+		viewDocument.createRoot( domEditor );
+		viewDocument.selection.removeAllRanges();
+		document.getSelection().removeAllRanges();
 
-		treeView.addObserver( MutationObserver );
-		mutationObserver = Array.from( treeView._observers )[ 0 ];
+		mutationObserver = viewDocument.addObserver( MutationObserver );
 
-		treeView.on( 'mutations', ( evt, mutations ) => lastMutations = mutations );
+		viewDocument.on( 'mutations', ( evt, mutations ) => lastMutations = mutations );
 
-		viewRoot = treeView.viewRoots.get( 'editor' );
+		viewRoot = viewDocument.getRoot();
 
-		viewRoot.insertChildren( 0, [
-			new Element( 'p', [], [ new Text( 'foo' ) ] ),
-			new Element( 'p', [], [ new Text( 'bar' ) ] )
-		] );
+		viewRoot.appendChildren( parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 
-		treeView.render();
+		viewDocument.render();
 	} );
 
 	afterEach( () => {
@@ -44,7 +41,7 @@ describe( 'MutationObserver', () => {
 	it( 'should handle typing', () => {
 		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
 
-		handleMutation();
+		mutationObserver.flush();
 
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
@@ -60,7 +57,7 @@ describe( 'MutationObserver', () => {
 		domB.appendChild( document.createTextNode( 'oo' ) );
 		domEditor.childNodes[ 0 ].appendChild( domB );
 
-		handleMutation();
+		mutationObserver.flush();
 
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
@@ -79,7 +76,7 @@ describe( 'MutationObserver', () => {
 		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foox';
 		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'fooxy';
 
-		handleMutation();
+		mutationObserver.flush();
 
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
@@ -98,7 +95,7 @@ describe( 'MutationObserver', () => {
 		domP.appendChild( domB );
 		domB.appendChild( domText );
 
-		handleMutation();
+		mutationObserver.flush();
 
 		expectDomEditorNotToChange();
 		expect( lastMutations.length ).to.equal( 1 );
@@ -107,30 +104,94 @@ describe( 'MutationObserver', () => {
 	} );
 
 	it( 'should be able to observe multiple roots', () => {
-		const domEditor2 = document.getElementById( 'editor2' );
+		const domAdditionalEditor = document.getElementById( 'additional' );
 
-		// Prepare editor2
-		treeView.createRoot( domEditor2, 'editor2' );
+		// Prepare AdditionalEditor
+		viewDocument.createRoot( domAdditionalEditor, 'additional' );
 
-		treeView.viewRoots.get( 'editor2' ).insertChildren( 0, [
-			new Element( 'p', [], [ new Text( 'foo' ) ] ),
-			new Element( 'p', [], [ new Text( 'bar' ) ] )
-		] );
+		viewDocument.viewRoots.get( 'additional' ).appendChildren(
+			parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
 
-		// Render editor2 (first editor has been rendered in the beforeEach function)
-		treeView.render();
+		// Render AdditionalEditor (first editor has been rendered in the beforeEach function)
+		viewDocument.render();
 
 		domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
-		domEditor2.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
+		domAdditionalEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom';
 
-		handleMutation();
+		mutationObserver.flush();
 
 		expect( lastMutations.length ).to.equal( 2 );
 	} );
 
-	function handleMutation() {
-		mutationObserver._onMutations( mutationObserver._mutationObserver.takeRecords() );
-	}
+	it( 'should fire nothing if there were no mutations', () => {
+		mutationObserver.flush();
+
+		expectDomEditorNotToChange();
+		expect( lastMutations ).to.be.null;
+	} );
+
+	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>' );
+
+		viewRoot.appendChildren( view );
+		viewDocument.selection.setTo( selection );
+
+		viewDocument.render();
+
+		const inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
+		inlineFiller.data += 'x';
+
+		mutationObserver.flush();
+
+		expect( lastMutations.length ).to.equal( 1 );
+		expect( lastMutations[ 0 ].type ).to.equal( 'children' );
+		expect( lastMutations[ 0 ].node ).to.equal( selection.getFirstPosition().parent );
+	} );
+
+	it( 'should have no inline filler in mutation', () => {
+		const { view, selection } = parse( '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+
+		viewRoot.appendChildren( view );
+		viewDocument.selection.setTo( selection );
+
+		viewDocument.render();
+
+		let inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
+		inlineFiller.data += 'x';
+
+		view.getChild( 1 ).appendChildren( parse( 'x' ) );
+		mutationObserver.flush();
+		viewDocument.render();
+
+		inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
+		inlineFiller.data += 'y';
+
+		mutationObserver.flush();
+
+		expect( lastMutations.length ).to.equal( 1 );
+		expect( lastMutations[ 0 ].type ).to.equal( 'text' );
+		expect( lastMutations[ 0 ].oldText ).to.equal( 'x' );
+		expect( lastMutations[ 0 ].newText ).to.equal( 'xy' );
+	} );
+
+	it( 'should have no block filler in mutation', () => {
+		viewRoot.appendChildren( parse( '<container:p></container:p>' ) );
+
+		viewDocument.render();
+
+		const domP = domEditor.childNodes[ 2 ];
+		domP.removeChild( domP.childNodes[ 0 ] );
+		domP.appendChild( document.createTextNode( 'foo' ) );
+
+		mutationObserver.flush();
+
+		expect( lastMutations.length ).to.equal( 1 );
+
+		expect( lastMutations[ 0 ].newChildren.length ).to.equal( 1 );
+		expect( lastMutations[ 0 ].newChildren[ 0 ].data ).to.equal( 'foo' );
+
+		expect( lastMutations[ 0 ].oldChildren.length ).to.equal( 0 );
+	} );
 
 	function expectDomEditorNotToChange() {
 		expect( domEditor.childNodes.length ).to.equal( 2 );

+ 3 - 3
packages/ckeditor5-engine/tests/treeview/observer/observer.js

@@ -12,11 +12,11 @@ import Observer from '/ckeditor5/engine/treeview/observer/observer.js';
 describe( 'Observer', () => {
 	describe( 'constructor', () => {
 		it( 'should create Observer with properties', () => {
-			const treeView = {};
-			const observer = new Observer( treeView );
+			const viewDocument = {};
+			const observer = new Observer( viewDocument );
 
 			expect( observer ).to.be.an.instanceof( Observer );
-			expect( observer ).to.have.property( 'treeView' ).that.equals( treeView );
+			expect( observer ).to.have.property( 'document' ).that.equals( viewDocument );
 			expect( observer ).to.have.property( 'isEnabled' ).that.is.false;
 		} );
 	} );

+ 147 - 0
packages/ckeditor5-engine/tests/treeview/observer/selectionobserver.js

@@ -0,0 +1,147 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treeview, browser-only */
+
+'use strict';
+
+import ViewRange from '/ckeditor5/engine/treeview/range.js';
+import ViewSelection from '/ckeditor5/engine/treeview/selection.js';
+import ViewDocument from '/ckeditor5/engine/treeview/document.js';
+import SelectionObserver from '/ckeditor5/engine/treeview/observer/selectionobserver.js';
+import MutationObserver from '/ckeditor5/engine/treeview/observer/mutationobserver.js';
+
+import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
+
+import { parse } from '/tests/engine/_utils/view.js';
+
+describe( 'SelectionObserver', () => {
+	let viewDocument, viewRoot, mutationObserver, selectionObserver, listenter;
+
+	before( () => {
+		listenter = Object.create( EmitterMixin );
+
+		viewDocument = new ViewDocument();
+
+		viewDocument.createRoot( document.getElementById( 'main' ) );
+
+		mutationObserver = viewDocument.addObserver( MutationObserver );
+		selectionObserver = viewDocument.addObserver( SelectionObserver );
+
+		viewRoot = viewDocument.getRoot();
+
+		viewRoot.appendChildren( parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
+
+		viewDocument.render();
+	} );
+
+	beforeEach( ( done ) => {
+		viewDocument.selection.removeAllRanges();
+		document.getSelection().removeAllRanges();
+
+		viewDocument.getObserver( SelectionObserver ).enable();
+
+		// Ensure selectionchange will not be fired.
+		setTimeout( () => done(), 100 );
+	} );
+
+	afterEach( () => {
+		listenter.stopListening();
+	} );
+
+	it( 'should fire selectionChange when it is the only change', ( done ) => {
+		listenter.listenTo( viewDocument, 'selectionChange', ( evt, data ) => {
+			expect( data ).to.have.property( 'domSelection' ).that.equals( document.getSelection() );
+
+			expect( data ).to.have.property( 'oldSelection' ).that.is.instanceof( ViewSelection );
+			expect( data.oldSelection.rangeCount ).to.equal( 0 );
+
+			expect( data ).to.have.property( 'newSelection' ).that.is.instanceof( ViewSelection );
+			expect( data.newSelection.rangeCount ).to.equal( 1 );
+
+			const newViewRange = data.newSelection.getFirstRange();
+			const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
+
+			expect( newViewRange.start.parent ).to.equal( viewFoo );
+			expect( newViewRange.start.offset ).to.equal( 1 );
+			expect( newViewRange.end.parent ).to.equal( viewFoo );
+			expect( newViewRange.end.offset ).to.equal( 2 );
+
+			done();
+		} );
+
+		changeDomSelection();
+	} );
+
+	it( 'should add only one listener to one document', ( done ) => {
+		// Add second roots to ensure that listener is added once.
+		viewDocument.createRoot( document.getElementById( 'additional' ), 'additional' );
+
+		listenter.listenTo( viewDocument, 'selectionChange', () => {
+			done();
+		} );
+
+		changeDomSelection();
+	} );
+
+	it( 'should not fire selectionChange on render', ( done ) => {
+		listenter.listenTo( viewDocument, 'selectionChange', () => {
+			throw 'selectionChange on render';
+		} );
+
+		setTimeout( () => done(), 70 );
+
+		const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
+		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
+		viewDocument.render();
+	} );
+
+	it( 'should not fired if observer is disabled', ( done ) => {
+		viewDocument.getObserver( SelectionObserver ).disable();
+
+		listenter.listenTo( viewDocument, 'selectionChange', () => {
+			throw 'selectionChange on render';
+		} );
+
+		setTimeout( () => done(), 70 );
+
+		changeDomSelection();
+	} );
+
+	it( 'should call render after selection change which reset selection if it was not changed', ( done ) => {
+		const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
+		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 0, viewBar, 1 ) );
+
+		listenter.listenTo( viewDocument, 'selectionChange', () => {
+			setTimeout( () => {
+				const domSelection = document.getSelection();
+
+				expect( domSelection.rangeCount ).to.equal( 1 );
+
+				const domRange = domSelection.getRangeAt( 0 );
+				const domBar = document.getElementById( 'main' ).childNodes[ 1 ].childNodes[ 0 ];
+
+				expect( domRange.startContainer ).to.equal( domBar );
+				expect( domRange.startOffset ).to.equal( 0 );
+				expect( domRange.endContainer ).to.equal( domBar );
+				expect( domRange.endOffset ).to.equal( 1 );
+
+				done();
+			} );
+		} );
+
+		changeDomSelection();
+	} );
+} );
+
+function changeDomSelection() {
+	const domSelection = document.getSelection();
+	domSelection.removeAllRanges();
+	const domFoo = document.getElementById( 'main' ).childNodes[ 0 ].childNodes[ 0 ];
+	const domRange = new Range();
+	domRange.setStart( domFoo, 1 );
+	domRange.setEnd( domFoo, 2 );
+	domSelection.addRange( domRange );
+}

+ 37 - 1
packages/ckeditor5-engine/tests/treeview/position.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
@@ -12,6 +12,10 @@ import Node from '/ckeditor5/engine/treeview/node.js';
 import Element from '/ckeditor5/engine/treeview/element.js';
 import Text from '/ckeditor5/engine/treeview/text.js';
 
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+
+import { parse } from '/tests/engine/_utils/view.js';
+
 describe( 'Position', () => {
 	const parentMock = {};
 
@@ -283,4 +287,36 @@ describe( 'Position', () => {
 			expect( position.compareWith( compared ) ).to.equal( 'DIFFERENT' );
 		} );
 	} );
+
+	describe( 'createBefore', () => {
+		it( 'should create positions before nodes', () => {
+			const { selection } = parse( '<p>[]<b></b></p>' );
+			const position = selection.getFirstPosition();
+			const nodeAfter = position.nodeAfter;
+
+			expect( Position.createBefore( nodeAfter ).isEqual( position ) ).to.be.true;
+		} );
+
+		it( 'should throw error if one try to create positions before root', () => {
+			expect( () => {
+				Position.createBefore( parse( '<p></p>' ) );
+			} ).to.throw( CKEditorError, /position-before-root/ );
+		} );
+	} );
+
+	describe( 'createAfter', () => {
+		it( 'should create positions after nodes', () => {
+			const { selection } = parse( '<p><b></b>[]</p>' );
+			const position = selection.getFirstPosition();
+			const nodeBefore = position.nodeBefore;
+
+			expect( Position.createAfter( nodeBefore ).isEqual( position ) ).to.be.true;
+		} );
+
+		it( 'should throw error if one try to create positions after root', () => {
+			expect( () => {
+				Position.createAfter( parse( '<p></p>' ) );
+			} ).to.throw( CKEditorError, /position-after-root/ );
+		} );
+	} );
 } );

+ 642 - 83
packages/ckeditor5-engine/tests/treeview/renderer.js

@@ -3,33 +3,38 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
-import Renderer from '/ckeditor5/engine/treeview/renderer.js';
 import ViewElement from '/ckeditor5/engine/treeview/element.js';
 import ViewText from '/ckeditor5/engine/treeview/text.js';
+import ViewRange from '/ckeditor5/engine/treeview/range.js';
+import Selection from '/ckeditor5/engine/treeview/selection.js';
 import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
+import Renderer from '/ckeditor5/engine/treeview/renderer.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { parse } from '/tests/engine/_utils/view.js';
+import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, BR_FILLER } from '/ckeditor5/engine/treeview/filler.js';
 
 describe( 'Renderer', () => {
-	let domConverter, renderer;
+	let selection, domConverter, renderer;
 
 	before( () => {
+		selection = new Selection();
 		domConverter = new DomConverter();
-		renderer = new Renderer( domConverter );
+		renderer = new Renderer( domConverter, selection );
 	} );
 
 	describe( 'markToSync', () => {
-		let viewNode;
+		let viewRoot;
 
 		beforeEach( () => {
-			viewNode = new ViewElement( 'p' );
+			viewRoot = new ViewElement( 'p' );
 
-			const domNode = document.createElement( 'p' );
-			domConverter.bindElements( domNode, viewNode );
-			viewNode.appendChildren( new ViewText( 'foo' ) );
+			const domRoot = document.createElement( 'p' );
+			domConverter.bindElements( domRoot, viewRoot );
+			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
 			renderer.markedTexts.clear();
 			renderer.markedAttributes.clear();
@@ -37,35 +42,35 @@ describe( 'Renderer', () => {
 		} );
 
 		it( 'should mark attributes which need update', () => {
-			viewNode.setAttribute( 'class', 'foo' );
+			viewRoot.setAttribute( 'class', 'foo' );
 
-			renderer.markToSync( 'ATTRIBUTES', viewNode );
+			renderer.markToSync( 'ATTRIBUTES', viewRoot );
 
-			expect( renderer.markedAttributes.has( viewNode ) ).to.be.true;
+			expect( renderer.markedAttributes.has( viewRoot ) ).to.be.true;
 		} );
 
 		it( 'should mark children which need update', () => {
-			viewNode.appendChildren( new ViewText( 'foo' ) );
+			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 
-			expect( renderer.markedChildren.has( viewNode ) ).to.be.true;
+			expect( renderer.markedChildren.has( viewRoot ) ).to.be.true;
 		} );
 
 		it( 'should not mark children if element has no corresponding node', () => {
-			// Overwrite viewNode with node without coresponding DOM node.
-			viewNode = new ViewElement( 'p' );
+			// Overwrite viewRoot with node without coresponding DOM node.
+			viewRoot = new ViewElement( 'p' );
 
-			viewNode.appendChildren( new ViewText( 'foo' ) );
+			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 
-			expect( renderer.markedTexts.has( viewNode ) ).to.be.false;
+			expect( renderer.markedTexts.has( viewRoot ) ).to.be.false;
 		} );
 
 		it( 'should mark text which need update', () => {
 			const viewText = new ViewText( 'foo' );
-			viewNode.appendChildren( viewText );
+			viewRoot.appendChildren( viewText );
 			viewText.data = 'bar';
 
 			renderer.markToSync( 'TEXT', viewText );
@@ -75,10 +80,10 @@ describe( 'Renderer', () => {
 
 		it( 'should not mark text if parent has no corresponding node', () => {
 			const viewText = new ViewText( 'foo' );
-			// Overwrite viewNode with node without coresponding DOM node.
-			viewNode = new ViewElement( 'p' );
+			// Overwrite viewRoot with node without coresponding DOM node.
+			viewRoot = new ViewElement( 'p' );
 
-			viewNode.appendChildren( viewText );
+			viewRoot.appendChildren( viewText );
 			viewText.data = 'bar';
 
 			renderer.markToSync( 'TEXT', viewText );
@@ -88,98 +93,101 @@ describe( 'Renderer', () => {
 
 		it( 'should throw if the type is unknown', () => {
 			expect( () => {
-				renderer.markToSync( 'UNKNOWN', viewNode );
+				renderer.markToSync( 'UNKNOWN', viewRoot );
 			} ).to.throw( CKEditorError, /^renderer-unknown-type/ );
 		} );
 	} );
 
 	describe( 'render', () => {
-		let viewNode, domNode;
+		let viewRoot, domRoot;
 
 		beforeEach( () => {
-			viewNode = new ViewElement( 'p' );
-			domNode = document.createElement( 'p' );
+			viewRoot = new ViewElement( 'div' );
+			domRoot = document.createElement( 'div' );
+			document.body.appendChild( domRoot );
 
-			domConverter.bindElements( domNode, viewNode );
+			domConverter.bindElements( domRoot, viewRoot );
 
 			renderer.markedTexts.clear();
 			renderer.markedAttributes.clear();
 			renderer.markedChildren.clear();
+
+			selection.removeAllRanges();
 		} );
 
 		it( 'should update attributes', () => {
-			viewNode.setAttribute( 'class', 'foo' );
+			viewRoot.setAttribute( 'class', 'foo' );
 
-			renderer.markToSync( 'ATTRIBUTES', viewNode );
+			renderer.markToSync( 'ATTRIBUTES', viewRoot );
 			renderer.render();
 
-			expect( domNode.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( domRoot.getAttribute( 'class' ) ).to.equal( 'foo' );
 
 			expect( renderer.markedAttributes.size ).to.equal( 0 );
 		} );
 
 		it( 'should remove attributes', () => {
-			viewNode.setAttribute( 'class', 'foo' );
-			domNode.setAttribute( 'id', 'bar' );
-			domNode.setAttribute( 'class', 'bar' );
+			viewRoot.setAttribute( 'class', 'foo' );
+			domRoot.setAttribute( 'id', 'bar' );
+			domRoot.setAttribute( 'class', 'bar' );
 
-			renderer.markToSync( 'ATTRIBUTES', viewNode );
+			renderer.markToSync( 'ATTRIBUTES', viewRoot );
 			renderer.render();
 
-			expect( domNode.getAttribute( 'class' ) ).to.equal( 'foo' );
-			expect( domNode.getAttribute( 'id' ) ).to.be.not.ok;
+			expect( domRoot.getAttribute( 'class' ) ).to.equal( 'foo' );
+			expect( domRoot.getAttribute( 'id' ) ).to.be.not.ok;
 
 			expect( renderer.markedAttributes.size ).to.equal( 0 );
 		} );
 
 		it( 'should add children', () => {
-			viewNode.appendChildren( new ViewText( 'foo' ) );
+			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].data ).to.equal( 'foo' );
 
 			expect( renderer.markedChildren.size ).to.equal( 0 );
 		} );
 
 		it( 'should remove children', () => {
-			viewNode.appendChildren( new ViewText( 'foo' ) );
+			viewRoot.appendChildren( new ViewText( 'foo' ) );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].data ).to.equal( 'foo' );
 
-			viewNode.removeChildren( 0, 1 );
+			viewRoot.removeChildren( 0, 1 );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 0 );
+			expect( domRoot.childNodes.length ).to.equal( 0 );
 
 			expect( renderer.markedChildren.size ).to.equal( 0 );
 		} );
 
 		it( 'should update text', () => {
 			const viewText = new ViewText( 'foo' );
-			viewNode.appendChildren( viewText );
+			viewRoot.appendChildren( viewText );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].data ).to.equal( 'foo' );
 
 			viewText.data = 'bar';
 
 			renderer.markToSync( 'TEXT', viewText );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ].data ).to.equal( 'bar' );
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].data ).to.equal( 'bar' );
 
 			expect( renderer.markedTexts.size ).to.equal( 0 );
 		} );
@@ -187,84 +195,635 @@ describe( 'Renderer', () => {
 		it( 'should not update text parent child list changed', () => {
 			const viewImg = new ViewElement( 'img' );
 			const viewText = new ViewText( 'foo' );
-			viewNode.appendChildren( [ viewImg, viewText ] );
+			viewRoot.appendChildren( [ viewImg, viewText ] );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.markToSync( 'TEXT', viewText );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 2 );
-			expect( domNode.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
-			expect( domNode.childNodes[ 1 ].data ).to.equal( 'foo' );
+			expect( domRoot.childNodes.length ).to.equal( 2 );
+			expect( domRoot.childNodes[ 0 ].tagName ).to.equal( 'IMG' );
+			expect( domRoot.childNodes[ 1 ].data ).to.equal( 'foo' );
 		} );
 
 		it( 'should not change text if it is the same during text rendering', () => {
 			const viewText = new ViewText( 'foo' );
-			viewNode.appendChildren( viewText );
+			viewRoot.appendChildren( viewText );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
 			// This should not be changed during the render.
-			const domText = domNode.childNodes[ 0 ];
+			const domText = domRoot.childNodes[ 0 ];
 
 			renderer.markToSync( 'TEXT', viewText );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ] ).to.equal( domText );
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ] ).to.equal( domText );
 		} );
 
 		it( 'should not change text if it is the same during children rendering', () => {
 			const viewText = new ViewText( 'foo' );
-			viewNode.appendChildren( viewText );
+			viewRoot.appendChildren( viewText );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
 			// This should not be changed during the render.
-			const domText = domNode.childNodes[ 0 ];
+			const domText = domRoot.childNodes[ 0 ];
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ] ).to.equal( domText );
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ] ).to.equal( domText );
 		} );
 
 		it( 'should not change element if it is the same', () => {
 			const viewImg = new ViewElement( 'img' );
-			viewNode.appendChildren( viewImg );
+			viewRoot.appendChildren( viewImg );
 
 			// This should not be changed during the render.
 			const domImg = document.createElement( 'img' );
-			domNode.appendChild( domImg );
+			domRoot.appendChild( domImg );
 
 			domConverter.bindElements( domImg, viewImg );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ] ).to.equal( domImg );
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ] ).to.equal( domImg );
 		} );
 
 		it( 'should change element if it is different', () => {
 			const viewImg = new ViewElement( 'img' );
-			viewNode.appendChildren( viewImg );
+			viewRoot.appendChildren( viewImg );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
+			renderer.markToSync( 'CHILDREN', viewRoot );
 			renderer.render();
 
 			const viewP = new ViewElement( 'p' );
-			viewNode.removeChildren( 0, 1 );
-			viewNode.appendChildren( viewP );
+			viewRoot.removeChildren( 0, 1 );
+			viewRoot.appendChildren( viewP );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].tagName ).to.equal( 'P' );
+		} );
+
+		it( 'should add and remove inline filler in case <p>foo<b>[]</b>bar</p>', () => {
+			const domSelection = document.getSelection();
+
+			// Step 1: <p>foo<b>"FILLER{}"</b></p>
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			expect( domRoot.childNodes.length ).to.equal( 1 );
+			expect( domRoot.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'p' );
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 3 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domP.childNodes[ 2 ].data ).to.equal( 'bar' );
+			expect( domP.childNodes[ 1 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 1 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 1 ].childNodes[ 0 ].data ).to.equal( INLINE_FILLER );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 1 ].childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Step 2: No mutation on second render
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'CHILDREN', viewP );
+
+			renderAndExpectNoChanges( renderer, domRoot );
+
+			// Step 3: <p>foo{}<b></b></p>
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) );
 
-			renderer.markToSync( 'CHILDREN', viewNode );
 			renderer.render();
 
-			expect( domNode.childNodes.length ).to.equal( 1 );
-			expect( domNode.childNodes[ 0 ].tagName ).to.equal( 'P' );
+			expect( domP.childNodes.length ).to.equal( 3 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domP.childNodes[ 2 ].data ).to.equal( 'bar' );
+			expect( domP.childNodes[ 1 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 1 ].childNodes.length ).to.equal( 0 );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 3 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Step 4: No mutation on second render
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.markToSync( 'CHILDREN', viewP );
+
+			renderAndExpectNoChanges( renderer, domRoot );
+		} );
+
+		it( 'should add and remove inline filler in case <p>[]<b>foo</b></p>', () => {
+			const domSelection = document.getSelection();
+
+			// Step 1: <p>"FILLER{}"<b>foo</b></p>
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p>[]<attribute:b>foo</attribute:b></container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( INLINE_FILLER );
+			expect( domP.childNodes[ 1 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 1 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 1 ].childNodes[ 0 ].data ).to.equal( 'foo' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Step 2: No mutation on second render
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+
+			// Step 3: <p><b>{}foo</b></p>
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets(
+				viewP.getChild( 0 ).getChild( 0 ), 0, viewP.getChild( 0 ).getChild( 0 ), 0 ) );
+
+			renderer.render();
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 0 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].childNodes[ 0 ].data ).to.equal( 'foo' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 0 ] .childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 0 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Step 4: No mutation on second render
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+		} );
+
+		it( 'should add and remove inline filler in case <p><b>foo</b>[]</p>', () => {
+			const domSelection = document.getSelection();
+
+			// Step 1: <p>"FILLER{}"<b>foo</b></p>
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p><attribute:b>foo</attribute:b>[]</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 0 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domP.childNodes[ 1 ].data ).to.equal( INLINE_FILLER );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 1 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Step 2: No mutation on second render
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+
+			// Step 3: <p><b>foo{}</b></p>
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets(
+				viewP.getChild( 0 ).getChild( 0 ), 3, viewP.getChild( 0 ).getChild( 0 ), 3 ) );
+
+			renderer.render();
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 0 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].childNodes[ 0 ].data ).to.equal( 'foo' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 0 ] .childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 3 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Step 4: No mutation on second render
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+		} );
+
+		it( 'should add and remove inline filler in case <p><b>foo</b>[]<b>bar</b></p>', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p><attribute:b>foo</attribute:b>[]<attribute:b>bar</attribute:b></container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 3 );
+			expect( domP.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 0 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domP.childNodes[ 1 ].data ).to.equal( INLINE_FILLER );
+			expect( domP.childNodes[ 2 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 2 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 2 ].childNodes[ 0 ].data ).to.equal( 'bar' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 1 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+		} );
+
+		it( 'should handle typing in empty block, do nothing if changes are already applied', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse( '<container:p>[]</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( isBlockFiller( domP.childNodes[ 0 ], BR_FILLER ) ).to.be.true;
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 0 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Remove filler and add text node to both DOM and View <p>x{}</p>
+			domP.removeChild( domP.childNodes[ 0 ] );
+			domP.appendChild( document.createTextNode( 'x' ) );
+
+			domSelection.removeAllRanges();
+			const domRange = new Range();
+			domRange.setStart( domP.childNodes[ 0 ], 1 );
+			domRange.collapse( true );
+			domSelection.addRange( domRange );
+
+			const viewText = new ViewText( 'x' );
+			viewP.appendChildren( viewText );
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+		} );
+
+		it( 'should handle typing in empty block, render if needed', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse( '<container:p>[]</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( isBlockFiller( domP.childNodes[ 0 ], BR_FILLER ) ).to.be.true;
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 0 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Add text node only in View <p>x{}</p>
+			const viewText = new ViewText( 'x' );
+			viewP.appendChildren( viewText );
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderer.render();
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( 'x' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+		} );
+
+		it( 'should handle removing last character', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse( '<container:p>x{}</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( 'x' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Remove text and add filler to both DOM and View <p>{}</p>
+			domP.removeChild( domP.childNodes[ 0 ] );
+			domP.appendChild( BR_FILLER( document ) );
+
+			domSelection.removeAllRanges();
+			const domRange = new Range();
+			domRange.setStart( domP.childNodes[ 0 ], 0 );
+			domRange.collapse( true );
+			domSelection.addRange( domRange );
+
+			viewP.removeChildren( 0 );
+
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
+
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+		} );
+
+		it( 'should handle typing in empty attribute, do nothing if changes are already applied', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p><attribute:b>[]</attribute:b>foo</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			const domB = domP.childNodes[ 0 ];
+			const viewB = viewP.getChild( 0 );
+
+			expect( domB.childNodes.length ).to.equal( 1 );
+			expect( domB.childNodes[ 0 ].data ).to.equal( INLINE_FILLER );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domB.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Add text node to both DOM and View <p><b>x</b>foo</p>
+			domB.childNodes[ 0 ].data += 'x';
+
+			domSelection.removeAllRanges();
+			const domRange = new Range();
+			domRange.setStart( domB.childNodes[ 0 ], INLINE_FILLER_LENGTH + 1 );
+			domRange.collapse( true );
+			domSelection.addRange( domRange );
+
+			const viewText = new ViewText( 'x' );
+			viewB.appendChildren( viewText );
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+		} );
+
+		it( 'should handle typing in empty attribute as a CHILDREN change, render if needed', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p><attribute:b>[]</attribute:b>foo</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			const domB = domP.childNodes[ 0 ];
+			const viewB = viewP.getChild( 0 );
+
+			expect( domB.childNodes.length ).to.equal( 1 );
+			expect( domB.childNodes[ 0 ].data ).to.equal( INLINE_FILLER );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domB.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Add text node only to View <p><b>x</b>foo</p>
+			const viewText = new ViewText( 'x' );
+			viewB.appendChildren( viewText );
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+
+			renderer.markToSync( 'CHILDREN', viewB );
+			renderer.render();
+
+			expect( domB.childNodes.length ).to.equal( 1 );
+			expect( domB.childNodes[ 0 ].data ).to.equal( INLINE_FILLER + 'x' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domB.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH + 1 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+		} );
+
+		it( 'should handle typing in empty attribute as a TEXT change, render if needed', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p><attribute:b>[]</attribute:b>foo</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 1 ].data ).to.equal( 'foo' );
+
+			const domB = domP.childNodes[ 0 ];
+			const viewB = viewP.getChild( 0 );
+
+			expect( domB.childNodes.length ).to.equal( 1 );
+			expect( domB.childNodes[ 0 ].data ).to.equal( INLINE_FILLER );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domB.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+
+			// Add text node only to View <p><b>x</b>foo</p>
+			const viewText = new ViewText( 'x' );
+			viewB.appendChildren( viewText );
+			selection.removeAllRanges();
+			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+
+			renderer.markToSync( 'TEXT', viewText );
+			renderer.render();
+
+			expect( domB.childNodes.length ).to.equal( 1 );
+			expect( domB.childNodes[ 0 ].data ).to.equal( INLINE_FILLER + 'x' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domB.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( INLINE_FILLER_LENGTH + 1 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+		} );
+
+		it( 'should handle not collapsed range', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p>fo{o<attribute:b>b}ar</attribute:b></container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 2 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( 'foo' );
+			expect( domP.childNodes[ 1 ].tagName.toLowerCase() ).to.equal( 'b' );
+			expect( domP.childNodes[ 1 ].childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 1 ].childNodes[ 0 ].data ).to.equal( 'bar' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP.childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 2 );
+			expect( domSelection.getRangeAt( 0 ).endContainer ).to.equal( domP.childNodes[ 1 ].childNodes[ 0 ] );
+			expect( domSelection.getRangeAt( 0 ).endOffset ).to.equal( 1 );
+
+			renderer.markToSync( 'CHILDREN', viewP );
+			renderAndExpectNoChanges( renderer, domRoot );
+		} );
+
+		it( 'should not add inline filler after text node', () => {
+			const domSelection = document.getSelection();
+
+			const { view: viewP, selection: newSelection } = parse( '<container:p>foo[]</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domP = domRoot.childNodes[ 0 ];
+
+			expect( domP.childNodes.length ).to.equal( 1 );
+			expect( domP.childNodes[ 0 ].data ).to.equal( 'foo' );
+
+			expect( domSelection.rangeCount ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).startContainer ).to.equal( domP );
+			expect( domSelection.getRangeAt( 0 ).startOffset ).to.equal( 1 );
+			expect( domSelection.getRangeAt( 0 ).collapsed ).to.be.true;
+		} );
+
+		it( 'should throw if there is no filler in expected position', () => {
+			const { view: viewP, selection: newSelection } = parse(
+				'<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+
+			viewRoot.appendChildren( viewP );
+			selection.setTo( newSelection );
+
+			renderer.markToSync( 'CHILDREN', viewRoot );
+			renderer.render();
+
+			const domB = domRoot.childNodes[ 0 ].childNodes[ 1 ];
+			const viewB = viewP.getChild( 1 );
+
+			expect( domB.childNodes[ 0 ].data ).to.equal( INLINE_FILLER );
+
+			// Remove filler.
+			domB.childNodes[ 0 ].data = '';
+
+			selection.removeAllRanges();
+			renderer.markToSync( 'CHILDREN', viewB );
+
+			expect( () => {
+				renderer.render();
+			} ).to.throw();
 		} );
 	} );
 } );
+
+function renderAndExpectNoChanges( renderer, domRoot ) {
+	const config = {
+		childList: true,
+		characterData: true,
+		characterDataOldValue: true,
+		subtree: true
+	};
+
+	const mutationObserver = new window.MutationObserver( () => {
+		throw 'There should be not mutations';
+	} );
+	mutationObserver.observe( domRoot, config );
+
+	renderer.render();
+
+	const records = mutationObserver.takeRecords();
+	mutationObserver.disconnect();
+	expect( records.length ).to.equal( 0 );
+}

+ 108 - 8
packages/ckeditor5-engine/tests/treeview/selection.js

@@ -83,35 +83,65 @@ describe( 'Selection', () => {
 	} );
 
 	describe( 'isCollapsed', () => {
-		it( 'should return true when all ranges are collapsed', () => {
-			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
-			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 15 );
-			selection.addRange( range1 );
-			selection.addRange( range2 );
+		it( 'should return true when there is single collapsed range', () => {
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
+			selection.addRange( range );
 
 			expect( selection.isCollapsed ).to.be.true;
 		} );
 
-		it( 'should return false when not all ranges are collapsed', () => {
+		it( 'should return false when there are multiple ranges', () => {
 			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
-			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 15 );
 			selection.addRange( range1 );
 			selection.addRange( range2 );
 
 			expect( selection.isCollapsed ).to.be.false;
 		} );
+
+		it( 'should return false when there is not collapsed range', () => {
+			const range = Range.createFromParentsAndOffsets( el, 15, el, 16 );
+			selection.addRange( range );
+
+			expect( selection.isCollapsed ).to.be.false;
+		} );
 	} );
 
 	describe( 'rangeCount', () => {
 		it( 'should return proper range count', () => {
 			expect( selection.rangeCount ).to.equal( 0 );
+
 			selection.addRange( range1 );
+
 			expect( selection.rangeCount ).to.equal( 1 );
+
 			selection.addRange( range2 );
+
 			expect( selection.rangeCount ).to.equal( 2 );
 		} );
 	} );
 
+	describe( 'isBackward', () => {
+		it( 'is defined by the last added range', () => {
+			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
+
+			selection.addRange( range1, true );
+			expect( selection ).to.have.property( 'isBackward', true );
+
+			selection.addRange( range2 );
+			expect( selection ).to.have.property( 'isBackward', false );
+		} );
+
+		it( 'is false when last range is collapsed', () => {
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
+
+			selection.addRange( range, true );
+
+			expect( selection.isBackward ).to.be.false;
+		} );
+	} );
+
 	describe( 'addRange', () => {
 		it( 'should add range to selection ranges', () => {
 			selection.addRange( range1 );
@@ -224,6 +254,56 @@ describe( 'Selection', () => {
 		} );
 	} );
 
+	describe( 'isEqual', () => {
+		it( 'should return true if selections equal', () => {
+			selection.addRange( range1 );
+			selection.addRange( range2 );
+
+			const otherSelection = new Selection();
+			otherSelection.addRange( range1 );
+			otherSelection.addRange( range2 );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if backward selections equal', () => {
+			selection.addRange( range1, true );
+
+			const otherSelection = new Selection();
+			otherSelection.addRange( range1, true );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return false if ranges count does not equal', () => {
+			selection.addRange( range1 );
+			selection.addRange( range2 );
+
+			const otherSelection = new Selection();
+			otherSelection.addRange( range1 );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if ranges do not equal', () => {
+			selection.addRange( range1 );
+
+			const otherSelection = new Selection();
+			otherSelection.addRange( range2 );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if directions do not equal', () => {
+			selection.addRange( range1 );
+
+			const otherSelection = new Selection();
+			otherSelection.addRange( range2, true );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'removeAllRanges', () => {
 		it( 'should remove all ranges and fire change event', ( done ) => {
 			selection.addRange( range1 );
@@ -263,6 +343,26 @@ describe( 'Selection', () => {
 		} );
 	} );
 
+	describe( 'setTo', () => {
+		it( 'should return true if selections equal', () => {
+			selection.addRange( range1 );
+
+			const otherSelection = new Selection();
+			otherSelection.addRange( range2 );
+			otherSelection.addRange( range3, true );
+
+			selection.setTo( otherSelection );
+
+			expect( selection.rangeCount ).to.equal( 2 );
+			expect( selection._ranges[ 0 ].isEqual( range2 ) ).to.be.true;
+			expect( selection._ranges[ 0 ] ).is.not.equal( range2 );
+			expect( selection._ranges[ 1 ].isEqual( range3 ) ).to.be.true;
+			expect( selection._ranges[ 1 ] ).is.not.equal( range3 );
+
+			expect( selection.anchor.isEqual( range3.end ) ).to.be.true;
+		} );
+	} );
+
 	describe( 'collapseToStart', () => {
 		it( 'should collapse to start position and fire change event', ( done ) => {
 			selection.setRanges( [ range1, range2, range3 ] );
@@ -308,4 +408,4 @@ describe( 'Selection', () => {
 			expect( fireSpy.notCalled ).to.be.true;
 		} );
 	} );
-} );
+} );

+ 0 - 137
packages/ckeditor5-engine/tests/treeview/treeview.js

@@ -1,137 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* bender-tags: treeview */
-
-'use strict';
-
-import TreeView from '/ckeditor5/engine/treeview/treeview.js';
-import Observer from '/ckeditor5/engine/treeview/observer/observer.js';
-import Renderer from '/ckeditor5/engine/treeview/renderer.js';
-import Writer from '/ckeditor5/engine/treeview/writer.js';
-import DomConverter from '/ckeditor5/engine/treeview/domconverter.js';
-
-import count from '/ckeditor5/utils/count.js';
-
-describe( 'TreeView', () => {
-	describe( 'constructor', () => {
-		it( 'should create TreeView with all properties', () => {
-			const treeView = new TreeView();
-
-			expect( count( treeView.domRoots ) ).to.equal( 0 );
-			expect( count( treeView.viewRoots ) ).to.equal( 0 );
-			expect( count( treeView._observers ) ).to.equal( 0 );
-			expect( treeView ).to.have.property( 'renderer' ).that.is.instanceOf( Renderer );
-			expect( treeView ).to.have.property( 'writer' ).that.is.instanceOf( Writer );
-			expect( treeView ).to.have.property( 'domConverter' ).that.is.instanceOf( DomConverter );
-		} );
-	} );
-
-	describe( 'createRoot', () => {
-		it( 'should create root', () => {
-			const domP = document.createElement( 'p' );
-			const domDiv = document.createElement( 'div' );
-			domDiv.setAttribute( 'id', 'editor' );
-			domDiv.appendChild( domP );
-
-			const treeView = new TreeView();
-			const ret = treeView.createRoot( domDiv, 'editor' );
-
-			expect( count( treeView.domRoots ) ).to.equal( 1 );
-			expect( count( treeView.viewRoots ) ).to.equal( 1 );
-
-			const domRoot = treeView.domRoots.get( 'editor' );
-			const viewRoot = treeView.viewRoots.get( 'editor' );
-
-			expect( ret ).to.equal( viewRoot );
-
-			expect( domRoot ).to.equal( domDiv );
-			expect( treeView.domConverter.getCorrespondingDom( viewRoot ) ).to.equal( domDiv );
-			expect( viewRoot.name ).to.equal( 'div' );
-			expect( viewRoot.getAttribute( 'id' ) ).to.equal( 'editor' );
-			expect( treeView.renderer.markedChildren.has( viewRoot ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'addObserver', () => {
-		let ObserverMock, treeView;
-
-		beforeEach( () => {
-			ObserverMock = class extends Observer {
-				constructor( treeView ) {
-					super( treeView );
-
-					this.enable = sinon.spy();
-					this.disable = sinon.spy();
-				}
-			};
-
-			treeView = new TreeView( document.createElement( 'div' ) );
-			treeView.renderer.render = sinon.spy();
-		} );
-
-		it( 'should be instantiated and enabled on adding', () => {
-			treeView.addObserver( ObserverMock );
-
-			expect( treeView._observers.size ).to.equal( 1 );
-
-			const observerMock = Array.from( treeView._observers )[ 0 ];
-
-			expect( observerMock ).to.have.property( 'treeView', treeView );
-			sinon.assert.calledOnce( observerMock.enable );
-		} );
-
-		it( 'should instantiate one observer only once', () => {
-			let instantiated = 0;
-			let enabled = 0;
-
-			class ObserverMock2 extends Observer {
-				constructor( treeView ) {
-					super( treeView );
-					instantiated++;
-				}
-
-				enable() {
-					enabled++;
-				}
-			}
-
-			treeView.addObserver( ObserverMock2 );
-			treeView.addObserver( ObserverMock2 );
-
-			expect( treeView._observers.size ).to.equal( 1 );
-			expect( instantiated ).to.equal( 1 );
-			expect( enabled ).to.equal( 1 );
-
-			treeView.addObserver( ObserverMock );
-			expect( treeView._observers.size ).to.equal( 2 );
-		} );
-
-		it( 'should instantiate child class of already registered observer', () => {
-			class ObserverMock extends Observer {
-				enable() {}
-			}
-			class ChildObserverMock extends ObserverMock {
-				enable() {}
-			}
-
-			treeView.addObserver( ObserverMock );
-			treeView.addObserver( ChildObserverMock );
-
-			expect( treeView._observers.size ).to.equal( 2 );
-		} );
-
-		it( 'should be disabled and re-enabled on render', () => {
-			treeView.addObserver( ObserverMock );
-			treeView.render();
-
-			const observerMock = Array.from( treeView._observers )[ 0 ];
-
-			sinon.assert.calledOnce( observerMock.disable );
-			sinon.assert.calledOnce( treeView.renderer.render );
-			sinon.assert.calledTwice( observerMock.enable );
-		} );
-	} );
-} );

+ 23 - 19
packages/ckeditor5-engine/tests/treeview/writer/breakattributes.js

@@ -3,14 +3,11 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
 import Writer from '/ckeditor5/engine/treeview/writer.js';
-import ContainerElement from '/ckeditor5/engine/treeview/containerelement.js';
-import Text from '/ckeditor5/engine/treeview/text.js';
-import Position from '/ckeditor5/engine/treeview/position.js';
 import { stringify, parse } from '/tests/engine/_utils/view.js';
 
 describe( 'Writer', () => {
@@ -34,25 +31,25 @@ describe( 'Writer', () => {
 	} );
 
 	describe( 'breakAttributes', () => {
-		it( 'should move position from begin of text node to the element', () => {
-			test( '<container:p>{}foobar</container:p>', '<container:p>[]foobar</container:p>' );
+		it( 'should not break text nodes if they are not in attribute elements - middle', () => {
+			test(
+				'<container:p>foo{}bar</container:p>',
+				'<container:p>foo{}bar</container:p>'
+			);
 		} );
 
-		it( 'should split text node', () => {
-			const text = new Text( 'foobar' );
-			const container = new ContainerElement( 'p', null, text );
-			const position = new Position( text, 3 );
-
-			const newPosition = writer.breakAttributes( position );
-
-			expect( container.getChildCount() ).to.equal( 2 );
-			expect( container.getChild( 0 ) ).to.be.instanceOf( Text ).and.have.property( 'data' ).that.equal( 'foo' );
-			expect( container.getChild( 1 ) ).to.be.instanceOf( Text ).and.have.property( 'data' ).that.equal( 'bar' );
-			expect( newPosition.isEqual( new Position( container, 1 ) ) ).to.be.true;
+		it( 'should not break text nodes if they are not in attribute elements - beginning', () => {
+			test(
+				'<container:p>{}foobar</container:p>',
+				'<container:p>{}foobar</container:p>'
+			);
 		} );
 
-		it( 'should move position from end of text node to the element', () => {
-			test( '<container:p>foobar{}</container:p>', '<container:p>foobar[]</container:p>' );
+		it( 'should not break text nodes if they are not in attribute elements #2 - end', () => {
+			test(
+				'<container:p>foobar{}</container:p>',
+				'<container:p>foobar{}</container:p>'
+			);
 		} );
 
 		it( 'should split attribute element', () => {
@@ -69,6 +66,13 @@ describe( 'Writer', () => {
 			);
 		} );
 
+		it( 'should stick selection in text node if it is in container', () => {
+			test(
+				'<container:p>foo{}<attribute:b:1>bar</attribute:b:1></container:p>',
+				'<container:p>foo{}<attribute:b:1>bar</attribute:b:1></container:p>'
+			);
+		} );
+
 		it( 'should split nested attributes', () => {
 			test(
 				'<container:p><attribute:b:1><attribute:u:1>foo{}bar</attribute:u:1></attribute:b:1></container:p>',

+ 23 - 16
packages/ckeditor5-engine/tests/treeview/writer/breakrange.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
@@ -24,7 +24,7 @@ describe( 'Writer', () => {
 	function test( input, expected ) {
 		const { view, selection } = parse( input );
 		const newRange = writer.breakRange( selection.getFirstRange() );
-		expect( stringify( view, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+		expect( stringify( view, newRange, { showType: true } ) ).to.equal( expected );
 	}
 
 	beforeEach( () => {
@@ -41,52 +41,59 @@ describe( 'Writer', () => {
 			} ).to.throw( 'treeview-writer-invalid-range-container' );
 		} );
 
-		it( 'should break at collapsed range and return collapsed one', () => {
+		it( 'should not break text nodes if they are not in attribute elements', () => {
 			test(
 				'<container:p>foo{}bar</container:p>',
-				'<container:p>foo[]bar</container:p>'
+				'<container:p>foo{}bar</container:p>'
+			);
+		} );
+
+		it( 'should break at collapsed range and return collapsed one', () => {
+			test(
+				'<container:p><attribute:b>foo{}bar</attribute:b></container:p>',
+				'<container:p><attribute:b>foo</attribute:b>[]<attribute:b>bar</attribute:b></container:p>'
 			);
 		} );
 
 		it( 'should break inside text node #1', () => {
 			test(
-				'<container:p>foo{bar}baz</container:p>',
-				'<container:p>foo[bar]baz</container:p>'
+				'<container:p><attribute:b>foo{bar}baz</attribute:b></container:p>',
+				'<container:p><attribute:b>foo</attribute:b>[<attribute:b>bar</attribute:b>]<attribute:b>baz</attribute:b></container:p>'
 			);
 		} );
 
 		it( 'should break inside text node #2', () => {
 			test(
-				'<container:p>foo{barbaz}</container:p>',
-				'<container:p>foo[barbaz]</container:p>'
+				'<container:p><attribute:b>foo{barbaz}</attribute:b></container:p>',
+				'<container:p><attribute:b>foo</attribute:b>[<attribute:b>barbaz</attribute:b>]</container:p>'
 			);
 		} );
 
 		it( 'should break inside text node #3', () => {
 			test(
-				'<container:p>foo{barbaz]</container:p>',
-				'<container:p>foo[barbaz]</container:p>'
+				'<container:p><attribute:b>foo{barbaz]</attribute:b></container:p>',
+				'<container:p><attribute:b>foo</attribute:b>[<attribute:b>barbaz</attribute:b>]</container:p>'
 			);
 		} );
 
 		it( 'should break inside text node #4', () => {
 			test(
-				'<container:p>{foo}barbaz</container:p>',
-				'<container:p>[foo]barbaz</container:p>'
+				'<container:p><attribute:b>{foo}barbaz</attribute:b></container:p>',
+				'<container:p>[<attribute:b>foo</attribute:b>]<attribute:b>barbaz</attribute:b></container:p>'
 			);
 		} );
 
 		it( 'should break inside text node #5', () => {
 			test(
-				'<container:p>[foo}barbaz</container:p>',
-				'<container:p>[foo]barbaz</container:p>'
+				'<container:p><attribute:b>[foo}barbaz</attribute:b></container:p>',
+				'<container:p>[<attribute:b>foo</attribute:b>]<attribute:b>barbaz</attribute:b></container:p>'
 			);
 		} );
 
 		it( 'should break placed inside different nodes', () => {
 			test(
-				'<container:p>foo{bar<attribute:b:1>baz}qux</attribute:b:1></container:p>',
-				'<container:p>foo[bar<attribute:b:1>baz</attribute:b:1>]<attribute:b:1>qux</attribute:b:1></container:p>'
+				'<container:p>foo{bar<attribute:b>baz}qux</attribute:b></container:p>',
+				'<container:p>foo{bar<attribute:b>baz</attribute:b>]<attribute:b>qux</attribute:b></container:p>'
 			);
 		} );
 	} );

+ 1 - 1
packages/ckeditor5-engine/tests/treeview/writer/insert.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 

+ 1 - 1
packages/ckeditor5-engine/tests/treeview/writer/mergeattributes.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 

+ 1 - 1
packages/ckeditor5-engine/tests/treeview/writer/remove.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 

+ 1 - 1
packages/ckeditor5-engine/tests/treeview/writer/unwrap.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 

+ 1 - 1
packages/ckeditor5-engine/tests/treeview/writer/wrap.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 

+ 2 - 2
packages/ckeditor5-engine/tests/treeview/writer/wrapposition.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';
 
@@ -112,4 +112,4 @@ describe( 'wrapPosition', () => {
 			'<container:p><attribute:b:1>foobar{}</attribute:b:1></container:p>'
 		);
 	} );
-} );
+} );

+ 1 - 1
packages/ckeditor5-engine/tests/treeview/writer/writer.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-/* bender-tags: treeview */
+/* bender-tags: treeview, browser-only */
 
 'use strict';