Bläddra i källkod

Merge pull request #397 from ckeditor/t/163

t/163: Selection handling
Piotrek Koszuliński 9 år sedan
förälder
incheckning
d6e24c13ef
50 ändrade filer med 3940 tillägg och 1114 borttagningar
  1. 43 10
      packages/ckeditor5-engine/src/treeview/attributeelement.js
  2. 11 2
      packages/ckeditor5-engine/src/treeview/containerelement.js
  3. 288 56
      packages/ckeditor5-engine/src/treeview/domconverter.js
  4. 2 2
      packages/ckeditor5-engine/src/treeview/element.js
  5. 176 0
      packages/ckeditor5-engine/src/treeview/filler.js
  6. 31 15
      packages/ckeditor5-engine/src/treeview/observer/mutationobserver.js
  7. 131 0
      packages/ckeditor5-engine/src/treeview/observer/selectionobserver.js
  8. 46 4
      packages/ckeditor5-engine/src/treeview/position.js
  9. 309 56
      packages/ckeditor5-engine/src/treeview/renderer.js
  10. 39 2
      packages/ckeditor5-engine/src/treeview/selection.js
  11. 54 26
      packages/ckeditor5-engine/src/treeview/treeview.js
  12. 27 2
      packages/ckeditor5-engine/tests/_utils/view.js
  13. 46 2
      packages/ckeditor5-engine/tests/treeview/attributeelement.js
  14. 11 0
      packages/ckeditor5-engine/tests/treeview/containerelement.js
  15. 0 586
      packages/ckeditor5-engine/tests/treeview/domconverter.js
  16. 327 0
      packages/ckeditor5-engine/tests/treeview/domconverter/binding.js
  17. 432 0
      packages/ckeditor5-engine/tests/treeview/domconverter/dom-to-view.js
  18. 31 0
      packages/ckeditor5-engine/tests/treeview/domconverter/domconverter.js
  19. 321 0
      packages/ckeditor5-engine/tests/treeview/domconverter/view-to-dom.js
  20. 134 0
      packages/ckeditor5-engine/tests/treeview/filler.js
  21. 0 0
      packages/ckeditor5-engine/tests/treeview/manual/__template__.html
  22. 27 0
      packages/ckeditor5-engine/tests/treeview/manual/immutable.js
  23. 4 0
      packages/ckeditor5-engine/tests/treeview/manual/immutable.md
  24. 0 4
      packages/ckeditor5-engine/tests/treeview/manual/init.html
  25. 0 24
      packages/ckeditor5-engine/tests/treeview/manual/init.js
  26. 0 13
      packages/ckeditor5-engine/tests/treeview/manual/init.md
  27. 28 0
      packages/ckeditor5-engine/tests/treeview/manual/inline-filler.js
  28. 8 0
      packages/ckeditor5-engine/tests/treeview/manual/inline-filler.md
  29. 5 7
      packages/ckeditor5-engine/tests/treeview/manual/mutationobserver.js
  30. 30 0
      packages/ckeditor5-engine/tests/treeview/manual/selectionobserver.js
  31. 10 0
      packages/ckeditor5-engine/tests/treeview/manual/selectionobserver.md
  32. 0 35
      packages/ckeditor5-engine/tests/treeview/manual/typing.js
  33. 0 8
      packages/ckeditor5-engine/tests/treeview/manual/typing.md
  34. 33 0
      packages/ckeditor5-engine/tests/treeview/manual/x-index.js
  35. 13 0
      packages/ckeditor5-engine/tests/treeview/manual/x-index.md
  36. 2 0
      packages/ckeditor5-engine/tests/treeview/observer/__template__.html
  37. 2 5
      packages/ckeditor5-engine/tests/treeview/observer/domeventobserver.js
  38. 1 3
      packages/ckeditor5-engine/tests/treeview/observer/focusobserver.js
  39. 1 3
      packages/ckeditor5-engine/tests/treeview/observer/keyobserver.js
  40. 0 2
      packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.html
  41. 89 28
      packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.js
  42. 147 0
      packages/ckeditor5-engine/tests/treeview/observer/selectionobserver.js
  43. 36 0
      packages/ckeditor5-engine/tests/treeview/position.js
  44. 641 82
      packages/ckeditor5-engine/tests/treeview/renderer.js
  45. 70 0
      packages/ckeditor5-engine/tests/treeview/selection.js
  46. 0 137
      packages/ckeditor5-engine/tests/treeview/treeview.js
  47. 0 0
      packages/ckeditor5-engine/tests/treeview/treeview/integration.js
  48. 0 0
      packages/ckeditor5-engine/tests/treeview/treeview/jumpoverinlinefiller.html
  49. 101 0
      packages/ckeditor5-engine/tests/treeview/treeview/jumpoverinlinefiller.js
  50. 233 0
      packages/ckeditor5-engine/tests/treeview/treeview/treeview.js

+ 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;

+ 11 - 2
packages/ckeditor5-engine/src/treeview/containerelement.js

@@ -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;
+	}
 }

+ 288 - 56
packages/ckeditor5-engine/src/treeview/domconverter.js

@@ -7,7 +7,13 @@
 
 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
@@ -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.TreeView} treeView TreeView instance we should inject quirks handling on.
+ */
+export function injectQuirksHandling( treeView ) {
+	treeView.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 );
+			}
+		}
+	}
+}

+ 31 - 15
packages/ckeditor5-engine/src/treeview/observer/mutationobserver.js

@@ -6,6 +6,7 @@
 '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
@@ -69,6 +70,16 @@ export default class MutationObserver extends Observer {
 		this._mutationObserver = new window.MutationObserver( this._onMutations.bind( this ) );
 	}
 
+	/**
+	 * Synchronously fires {@link engine.treeView.TreeView#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,27 +174,19 @@ 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
 			} );
 		}

+ 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.TreeView#selection view selection}. Selection observer fires
+ * {@link engine.treeView.TreeView#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( treeView ) {
+		super( treeView );
+
+		/**
+		 * 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.TreeView#selectionChange} event is fired.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.observer.MutationObserver} engine.treeView.observer.SelectionObserver#mutationObserver
+		 */
+		this.mutationObserver = treeView.getObserver( MutationObserver );
+
+		/**
+		 * Reference to the {@link engine.treeView.TreeView} object.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.TreeView} engine.treeView.observer.SelectionObserver#treeView
+		 */
+		this.treeView = treeView;
+
+		/**
+		 * Reference to the TreeView {@link engine.treeView.Selection} object used to compare new selection with it.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.TreeView} engine.treeView.observer.SelectionObserver#selection
+		 */
+		this.selection = treeView.selection;
+
+		/**
+		 * Reference to the {@link engine.treeView.TreeView#domConverter}.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.DomConverter} engine.treeView.observer.SelectionObserver#domConverter
+		 */
+		this.domConverter = treeView.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.TreeView#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.treeView.fire( 'selectionChange', {
+			oldSelection: this.selection,
+			newSelection: newViewSelection,
+			domSelection: domSelection
+		} );
+
+		this.treeView.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.TreeView#selectionChange
+ * @param {Object} data
+ * @param {engine.treeView.Selection} data.oldSelection Old View selection which is
+ * {@link engine.treeView.TreeView#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;
 	}
 }

+ 39 - 2
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;
@@ -206,6 +206,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 +262,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

+ 54 - 26
packages/ckeditor5-engine/src/treeview/treeview.js

@@ -5,12 +5,14 @@
 
 '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.
@@ -37,6 +39,14 @@ export default class TreeView {
 		 */
 		this.domRoots = new Map();
 
+		/**
+		 * Selection done on this document.
+		 *
+		 * @readonly
+		 * @member {engine.treeView.Selection} engine.treeView.TreeView#selection
+		 */
+		this.selection = new Selection();
+
 		/**
 		 * Tree View writer.
 		 *
@@ -68,15 +78,17 @@ export default class TreeView {
 		 * @readonly
 		 * @member {engine.treeView.Renderer} engine.treeView.TreeView#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.TreeView_#observers
 		 */
-		this._observers = new Set();
+		this._observers = new Map();
+
+		injectQuirksHandling( this );
 	}
 
 	/**
@@ -90,21 +102,36 @@ 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;
+	}
+
+	/**
+	 * 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 );
 	}
 
 	/**
@@ -117,10 +144,10 @@ export default class TreeView {
 	 * 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 );
 
@@ -133,38 +160,39 @@ 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.TreeView#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 );

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

@@ -123,7 +123,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;
@@ -182,7 +182,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  * @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();
@@ -844,3 +844,28 @@ class ViewStringify {
 		return attributes.join( ' ' );
 	}
 }
+
+export function setData( treeView, data ) {
+	let view, selection;
+
+	const result = parse( data );
+
+	if ( result.view && result.selection ) {
+		selection = result.selection;
+		view = result.view;
+	} else {
+		view = result;
+	}
+
+	const root = treeView.getRoot();
+	root.removeChildren( 0, root.getChildCount() );
+	root.appendChildren( view );
+
+	if ( selection ) {
+		treeView.selection.setTo( selection );
+	}
+}
+
+export function getData( treeView ) {
+	return stringify( treeView.getRoot(), treeView.selection );
+}

+ 46 - 2
packages/ckeditor5-engine/tests/treeview/attributeelement.js

@@ -9,7 +9,7 @@
 
 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;
+		} );
+	} );
 } );

+ 11 - 0
packages/ckeditor5-engine/tests/treeview/containerelement.js

@@ -9,6 +9,7 @@
 
 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;
+		} );
+	} );
 } );

+ 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 */
+
+'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 */
+
+'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 */
+
+'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 */
+
+'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/mutationobserver.html → packages/ckeditor5-engine/tests/treeview/manual/__template__.html


+ 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 TreeView from '/ckeditor5/engine/treeview/treeview.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 treeView = new TreeView();
+treeView.createRoot( document.getElementById( 'editor' ) );
+
+treeView.addObserver( MutationObserver );
+treeView.addObserver( SelectionObserver );
+treeView.addObserver( KeyObserver );
+
+setData( treeView,
+	'<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>' );
+
+treeView.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 TreeView from '/ckeditor5/engine/treeview/treeview.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 treeView = new TreeView();
+treeView.createRoot( document.getElementById( 'editor' ) );
+
+treeView.addObserver( MutationObserver );
+treeView.addObserver( SelectionObserver );
+treeView.addObserver( KeyObserver );
+
+setData( treeView,
+	'<container:p><attribute:strong>foo</attribute:strong>[]<attribute:strong>bar</attribute:strong></container:p>' );
+
+treeView.on( 'selectionChange', ( evt, data ) => {
+	treeView.selection.setTo( data.newSelection );
+} );
+
+treeView.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 - 7
packages/ckeditor5-engine/tests/treeview/manual/mutationobserver.js

@@ -8,20 +8,18 @@
 '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';
+import { setData } from '/tests/engine/_utils/view.js';
 
 const treeView = new TreeView();
-treeView.createRoot( document.getElementById( 'editor' ), 'editor' );
+treeView.createRoot( document.getElementById( 'editor' ) );
 
 treeView.on( 'mutations', ( evt, mutations ) => console.log( mutations ) );
 
 treeView.addObserver( MutationObserver );
 
-treeView.viewRoots.get( 'editor' ).appendChildren( [
-	new Element( 'p', [], [ new Text( 'foo' ) ] ),
-	new Element( 'p', [], [ new Text( 'bom' ) ] )
-] );
+setData( treeView,
+	'<container:p>foo</container:p>' +
+	'<container:p>bar</container:p>' );
 
 treeView.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 TreeView from '/ckeditor5/engine/treeview/treeview.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 treeView = new TreeView();
+treeView.createRoot( document.getElementById( 'editor' ) );
+
+treeView.addObserver( MutationObserver );
+treeView.addObserver( SelectionObserver );
+
+setData( treeView,
+	'<container:p><attribute:b>foo</attribute:b>bar</container:p>' +
+	'<container:p>bom</container:p>' );
+
+treeView.on( 'selectionChange', ( evt, data ) => {
+	console.log( data );
+	treeView.selection.setTo( data.newSelection );
+} );
+
+treeView.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 TreeView from '/ckeditor5/engine/treeview/treeview.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 treeView = new TreeView();
+treeView.createRoot( document.getElementById( 'editor' ) );
+
+treeView.addObserver( MutationObserver );
+treeView.addObserver( SelectionObserver );
+
+setData( treeView,
+	'<container:p>fo{}o</container:p>' +
+	'<container:p></container:p>' +
+	'<container:p><attribute:strong></attribute:strong></container:p>' +
+	'<container:p>bar</container:p>' );
+
+treeView.on( 'selectionChange', ( evt, data ) => {
+	const node = data.newSelection.getFirstPosition().parent;
+	console.log( node.name ? node.name : node._data );
+	treeView.selection.setTo( data.newSelection );
+} );
+
+treeView.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
+```

+ 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>

+ 2 - 5
packages/ckeditor5-engine/tests/treeview/observer/domeventobserver.js

@@ -95,10 +95,9 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 
 		treeView.createRoot( domElement, 'root' );
-		treeView.addObserver( ClickObserver );
+		const testObserver = treeView.addObserver( ClickObserver );
 		treeView.on( 'click', evtSpy );
 
-		const testObserver = Array.from( treeView._observers )[ 0 ];
 		testObserver.disable();
 
 		domElement.dispatchEvent( domEvent );
@@ -113,11 +112,9 @@ describe( 'DomEventObserver', () => {
 		const evtSpy = sinon.spy();
 
 		treeView.createRoot( domElement, 'root' );
-		treeView.addObserver( ClickObserver );
+		const testObserver = treeView.addObserver( ClickObserver );
 		treeView.on( 'click', evtSpy );
 
-		const testObserver = Array.from( treeView._observers )[ 0 ];
-
 		testObserver.disable();
 
 		domElement.dispatchEvent( domEvent );

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

@@ -15,9 +15,7 @@ describe( 'FocusObserver', () => {
 
 	beforeEach( () => {
 		treeView = new TreeView();
-		treeView.addObserver( FocusObserver );
-
-		observer = Array.from( treeView._observers )[ 0 ];
+		observer = treeView.addObserver( FocusObserver );
 	} );
 
 	it( 'should define domEventType', () => {

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

@@ -16,9 +16,7 @@ describe( 'KeyObserver', () => {
 
 	beforeEach( () => {
 		treeView = new TreeView();
-		treeView.addObserver( KeyObserver );
-
-		observer = Array.from( treeView._observers )[ 0 ];
+		observer = treeView.addObserver( KeyObserver );
 	} );
 
 	it( 'should define domEventType', () => {

+ 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>

+ 89 - 28
packages/ckeditor5-engine/tests/treeview/observer/mutationobserver.js

@@ -8,31 +8,28 @@
 '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';
+import { parse } from '/tests/engine/_utils/view.js';
 
 describe( 'MutationObserver', () => {
 	let domEditor, treeView, viewRoot, mutationObserver, lastMutations;
 
 	beforeEach( () => {
 		treeView = new TreeView();
-		domEditor = document.getElementById( 'editor' );
+		domEditor = document.getElementById( 'main' );
 		lastMutations = null;
 
-		treeView.createRoot( domEditor, 'editor' );
+		treeView.createRoot( domEditor );
+		treeView.selection.removeAllRanges();
+		document.getSelection().removeAllRanges();
 
-		treeView.addObserver( MutationObserver );
-		mutationObserver = Array.from( treeView._observers )[ 0 ];
+		mutationObserver = treeView.addObserver( MutationObserver );
 
 		treeView.on( 'mutations', ( evt, mutations ) => lastMutations = mutations );
 
-		viewRoot = treeView.viewRoots.get( 'editor' );
+		viewRoot = treeView.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();
 	} );
@@ -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
+		treeView.createRoot( domAdditionalEditor, 'additional' );
 
-		treeView.viewRoots.get( 'editor2' ).insertChildren( 0, [
-			new Element( 'p', [], [ new Text( 'foo' ) ] ),
-			new Element( 'p', [], [ new Text( 'bar' ) ] )
-		] );
+		treeView.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)
+		// Render AdditionalEditor (first editor has been rendered in the beforeEach function)
 		treeView.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 );
+		treeView.selection.setTo( selection );
+
+		treeView.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 );
+		treeView.selection.setTo( selection );
+
+		treeView.render();
+
+		let inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ];
+		inlineFiller.data += 'x';
+
+		view.getChild( 1 ).appendChildren( parse( 'x' ) );
+		mutationObserver.flush();
+		treeView.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>' ) );
+
+		treeView.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 );

+ 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 */
+
+'use strict';
+
+import ViewRange from '/ckeditor5/engine/treeview/range.js';
+import ViewSelection from '/ckeditor5/engine/treeview/selection.js';
+import TreeView from '/ckeditor5/engine/treeview/treeview.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 treeView, viewRoot, mutationObserver, selectionObserver, listenter;
+
+	before( () => {
+		listenter = Object.create( EmitterMixin );
+
+		treeView = new TreeView();
+
+		treeView.createRoot( document.getElementById( 'main' ) );
+
+		mutationObserver = treeView.addObserver( MutationObserver );
+		selectionObserver = treeView.addObserver( SelectionObserver );
+
+		viewRoot = treeView.getRoot();
+
+		viewRoot.appendChildren( parse( '<container:p>foo</container:p><container:p>bar</container:p>' ) );
+
+		treeView.render();
+	} );
+
+	beforeEach( ( done ) => {
+		treeView.selection.removeAllRanges();
+		document.getSelection().removeAllRanges();
+
+		treeView.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( treeView, '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 = treeView.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.
+		treeView.createRoot( document.getElementById( 'additional' ), 'additional' );
+
+		listenter.listenTo( treeView, 'selectionChange', () => {
+			done();
+		} );
+
+		changeDomSelection();
+	} );
+
+	it( 'should not fire selectionChange on render', ( done ) => {
+		listenter.listenTo( treeView, 'selectionChange', () => {
+			throw 'selectionChange on render';
+		} );
+
+		setTimeout( () => done(), 70 );
+
+		const viewBar = treeView.getRoot().getChild( 1 ).getChild( 0 );
+		treeView.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
+		treeView.render();
+	} );
+
+	it( 'should not fired if observer is disabled', ( done ) => {
+		treeView.getObserver( SelectionObserver ).disable();
+
+		listenter.listenTo( treeView, '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 = treeView.getRoot().getChild( 1 ).getChild( 0 );
+		treeView.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 0, viewBar, 1 ) );
+
+		listenter.listenTo( treeView, '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 );
+}

+ 36 - 0
packages/ckeditor5-engine/tests/treeview/position.js

@@ -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/ );
+		} );
+	} );
 } );

+ 641 - 82
packages/ckeditor5-engine/tests/treeview/renderer.js

@@ -7,29 +7,34 @@
 
 '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 );
+}

+ 70 - 0
packages/ckeditor5-engine/tests/treeview/selection.js

@@ -224,6 +224,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 +313,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 ] );

+ 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 );
-		} );
-	} );
-} );

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


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


+ 101 - 0
packages/ckeditor5-engine/tests/treeview/treeview/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 */
+
+'use strict';
+
+import ViewRange from '/ckeditor5/engine/treeview/range.js';
+import TreeView from '/ckeditor5/engine/treeview/treeview.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( 'TreeView', () => {
+	let treeView;
+
+	beforeEach( () => {
+		treeView = new TreeView();
+
+		treeView.addObserver( KeyObserver );
+		treeView.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( treeView, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			treeView.render();
+
+			treeView.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: treeView.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( treeView, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			treeView.render();
+
+			treeView.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: treeView.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( treeView, '<container:p>foo<attribute:b>{x}</attribute:b>bar</container:p>' );
+			treeView.render();
+
+			treeView.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: treeView.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( treeView, '<container:p>foo<attribute:b>{}x</attribute:b>bar</container:p>' );
+			treeView.render();
+
+			treeView.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: treeView.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( treeView, '<container:p>foo<attribute:b>[]</attribute:b>bar</container:p>' );
+			treeView.render();
+
+			// '<container:p>foo<attribute:b>x{}</attribute:b>bar</container:p>'
+			const viewB = treeView.selection.getFirstPosition().parent;
+			const viewTextX = parse( 'x' );
+			viewB.appendChildren( viewTextX );
+			treeView.selection.removeAllRanges();
+			treeView.selection.addRange( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) );
+			treeView.render();
+
+			treeView.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: treeView.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;
+		} );
+	} );
+} );

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

@@ -0,0 +1,233 @@
+/**
+ * @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', () => {
+	let ObserverMock, ObserverMockGlobalCount, instantiated, enabled;
+
+	beforeEach( () => {
+		instantiated = 0;
+		enabled = 0;
+
+		ObserverMock = class extends Observer {
+			constructor( treeView ) {
+				super( treeView );
+
+				this.enable = sinon.spy();
+				this.disable = sinon.spy();
+				this.observe = sinon.spy();
+			}
+		};
+
+		ObserverMockGlobalCount = class extends Observer {
+			constructor( treeView ) {
+				super( treeView );
+				instantiated++;
+
+				this.observe = sinon.spy();
+			}
+
+			enable() {
+				enabled++;
+			}
+		};
+	} );
+
+	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;
+		} );
+
+		it( 'should call observe on each observer', () => {
+			const treeView = new TreeView( document.createElement( 'div' ) );
+			treeView.renderer.render = sinon.spy();
+
+			const domDiv1 = document.createElement( 'div' );
+			domDiv1.setAttribute( 'id', 'editor' );
+
+			const domDiv2 = document.createElement( 'div' );
+			domDiv2.setAttribute( 'id', 'editor' );
+
+			const observerMock = treeView.addObserver( ObserverMock );
+			const observerMockGlobalCount = treeView.addObserver( ObserverMockGlobalCount );
+
+			treeView.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 treeView = new TreeView();
+			const ret = treeView.createRoot( domDiv );
+
+			expect( count( treeView.domRoots ) ).to.equal( 1 );
+			expect( count( treeView.viewRoots ) ).to.equal( 1 );
+
+			const domRoot = treeView.domRoots.get( 'main' );
+			const viewRoot = treeView.viewRoots.get( 'main' );
+
+			expect( ret ).to.equal( viewRoot );
+
+			expect( domRoot ).to.equal( domDiv );
+		} );
+	} );
+
+	describe( 'getRoot', () => {
+		it( 'should return "main" root', () => {
+			const treeView = new TreeView();
+			treeView.createRoot( document.createElement( 'div' ) );
+
+			expect( count( treeView.viewRoots ) ).to.equal( 1 );
+
+			expect( treeView.getRoot() ).to.equal( treeView.viewRoots.get( 'main' ) );
+		} );
+
+		it( 'should return named root', () => {
+			const treeView = new TreeView();
+			treeView.createRoot( document.createElement( 'h1' ), 'header' );
+
+			expect( count( treeView.viewRoots ) ).to.equal( 1 );
+
+			expect( treeView.getRoot( 'header' ) ).to.equal( treeView.viewRoots.get( 'header' ) );
+		} );
+	} );
+
+	describe( 'addObserver', () => {
+		let treeView;
+
+		beforeEach( () => {
+			treeView = new TreeView( document.createElement( 'div' ) );
+			treeView.renderer.render = sinon.spy();
+		} );
+
+		it( 'should be instantiated and enabled on adding', () => {
+			const observerMock = treeView.addObserver( ObserverMock );
+
+			expect( treeView._observers.size ).to.equal( 1 );
+
+			expect( observerMock ).to.have.property( 'treeView', treeView );
+			sinon.assert.calledOnce( observerMock.enable );
+		} );
+
+		it( 'should return observer instance ever time addObserver is called', () => {
+			const observerMock1 = treeView.addObserver( ObserverMock );
+			const observerMock2 = treeView.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', () => {
+			treeView.addObserver( ObserverMockGlobalCount );
+			treeView.addObserver( ObserverMockGlobalCount );
+
+			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', () => {
+			const observerMock = treeView.addObserver( ObserverMock );
+			treeView.render();
+
+			sinon.assert.calledOnce( observerMock.disable );
+			sinon.assert.calledOnce( treeView.renderer.render );
+			sinon.assert.calledTwice( observerMock.enable );
+		} );
+
+		it( 'should call observe on each root', () => {
+			treeView.createRoot( document.createElement( 'div' ), 'root1' );
+			treeView.createRoot( document.createElement( 'div' ), 'root2' );
+
+			const observerMock = treeView.addObserver( ObserverMock );
+
+			sinon.assert.calledTwice( observerMock.observe );
+		} );
+	} );
+
+	describe( 'getObserver', () => {
+		it( 'should return observer it it is added', () => {
+			const treeView = new TreeView();
+
+			const addedObserverMock = treeView.addObserver( ObserverMock );
+			const getObserverMock = treeView.getObserver( ObserverMock );
+
+			expect( getObserverMock ).to.be.instanceof( ObserverMock );
+			expect( getObserverMock ).to.equal( addedObserverMock );
+		} );
+
+		it( 'should return undefined if observer is not added', () => {
+			const treeView = new TreeView();
+			const getObserverMock = treeView.getObserver( ObserverMock );
+
+			expect( getObserverMock ).to.be.undefined();
+		} );
+	} );
+} );